Testing PHP

Fixing a bug should not cause other bugs. But having a maintainable code is not achieved by running tests rather than writing them. TDD guides us in the direction of a SOLID, decoupled, clean and sustainable code. A well organised, clean architecture also helps a lot.

TDD is more debugging than testing. It is a common opinion that we should not ask for permission to apply TDD because we do not ask for permission to debug our code. During TDD we are just debugging with Tests which we can use later for Test Automation. A very useful side-effect, isn't it?

TDD is knowing everything worked one minute ago, but in every minute. It may not have enough features to deploy but that is a business decision.

We won't read a 600 page documentation if there are easy to understand examples. Well written tests are the best documentation because they are up-to-date and easy to understand examples of how to use the tested code.

Tests are the part of the system. We should apply the same principles to write decoupled tests to avoid the fragile tests problem.

It may seem that TDD slows down implementing new features, but we are spending much less time with classic debugging. Later when we will edit the code, testing will pay off anyways with Test Automation, not talking about the saved time on a decoupled, clean, solid and sustainable code.

Testing makes us confident and makes Devs, Ops and QA become good Friends.

Most important Test types

Unit Test

Unit Tests are isolated tests and defining the specification of the units.

The idea behind unit testing is to test each part of the program and show that the individual parts are correct.

Unit Tests can help to keep a unit's original and single function in the long run.

Unit Tests are the least expensive, it is an isolated test so it has a low cost to perform and maintain. The implementation of a Unit Test during the TDD Workflow has a very low cost and a lot of benefits. Usually the only type of test I write during TDD.

Integration Test

Integration Tests are non-isolated Tests and defining the specification of the unit integrations.

The idea behind integration testing is to show that the units are working fine together.

Integration Tests may depend on multiple modules. They are more expensive than Unit Tests.

Functional Test

Functional Tests check through the entire software that it has all the required functionality that's specified within its functional requirements.

The idea behind functional testing is to focus on the business requirements of an application.

Functional Tests are more expensive than Integration Tests.

End-to-end Test

End-to-end Tests are replicating a user behavior in a complete application environment.

The idea behind end-to-end testing is to verify that various user flows work as expected.

End-to-end Tests are pretty expensive to perform because they need a complete application environment. They are very expensive because user flow depends on all the production layers below and business decisions can affect on them too. Just think about re-skinning the UI.

Manual Test

Manual Tests are still worth doing. It is very helpful to uncover UI issues or verify complex user workflows.

The idea behind manual testing is to save the cost of implementing and maintaining edge cases of Automated Tests.

Manual Testing is expensive by nature.

Other Tests

There are other types of tests that are out of the scope of this article, like: Acceptance testing, Performance testing, Smoke testing, Penetration testing, etc.

TDD

The TDD workflow

Just do everything the same way as usual except debugging. :)

  1. Before you start to implement the first piece of production code, write a test for it.
  2. Write production code to fail the test. A missing class or a bad static return value is a good start. It is great to prove the test does not pass yet.
  3. Add incremental changes to pass the test.

Get to the next piece of production code but start over and for first write a test for it, make it fail, and add incremental changes that add up to a final implementation which will pass... and so on... test, fail, pass!

Write tests only for behavior (public methods) and not the implementation (private methods). Try to expect on something calling i.e. a public method and write a test for your expectation.

TDD helps to reduce the cost of bug fixes since the bugs are identified during the early phases of the development life-cycle. It helps to increase efficiency by writing the minimum required code. Yes, TDD is self policing but the goal is to think about the test cases, fail them for first and then pass all of them with the least amount of code necessary. And later not think about them at all. It helps to write clean code too.

The bug fix workflow

Bugs will never haunt again, again.

  1. Write a test for the bug.
  2. Test the bug that it fails the test.
  3. Make incremental changes to pass the test.

Common test case / assertion indicators

  • Arguments
  • Conditions (test each case)
  • Loops (test at least for zero and multiple cycles)
  • Return, Yield
  • Division
  • Debugging something manually during writing production code

Tips

  • Test every possible case not only the case that is important for your business logic.
  • Using an API, save the response and test the downloaded string / file.
  • Legacy spaghetti code bug fixing: create a function for the affected code and apply the Bug Fix Workflow.
  • We can lower the cost with Dependency Injection (and depending on abstractions). Writing production code with Dependencies not hard-coded is beneficial because an object parameter can be mocked in the test which can create an isolated environment.
  • We can lower the cost with Dependency Isolation. Separating a method into a regular method and an other method (maybe into another class) with only primitive arguments is  a good idea. If the dependency changes we do not have to rewrite the Test for the method with the primitive arguments.

SUT - testing non public methods

The System Under Test (SUT) from a Unit Testing perspective represents all of the actors (i.e one or more classes) in a test that are not Test Doubles (i.e mocks). It means that you have to decide what is the subject of the test (unit) and should not mock the subject being tested.

In case of a regular service class you should designate the whole class as subject and should not mock the class. In case of a helper service like a simple calculator methods you could designate the methods as units.

We are not testing the private & protected methods directly, because we are testing only behaviour and not implementation. What we usually want to test are the public methods of our class. Everything else is an implementation detail for our class and they should not "break" our tests if we change then. If there is significant functionality that is hidden behind private or protected access, that might be a warning sign that there's another class in there struggling to get out.

Maybe there is some company policy when you really need to test a private or a protected method. In this case (which I do not recommend) you could use an Access Modifier hack:


trait AccessModifierTrait { /** * Call protected/private method of a class. * * @param object &$object Instantiated object that we will run method on. * @param string $methodName Method name to call * @param array $parameters Array of parameters to pass into method. * * @return mixed Method return. */ public function invokeMethod(&$object, $methodName, array $parameters = []) { $reflection = new \ReflectionClass(get_class($object)); $method = $reflection->getMethod($methodName); $method->setAccessible(true); return $method->invokeArgs($object, $parameters); } }

as following:


... use AccessModifierTrait; ... $myMock = $this->createMock(MyClassContainsNonPublicMethod::class); $result = $this->invokeMethod($myMock, 'myPrivateMethodToCall', ['parameterForPrivateMethod']); ...

Another similar edge case I can imagine is depending on a less well written library in a legacy code when mocking the test subject is pragmatic in my opinion (however I do not recommend it) when a constructor's logic is very important but it's effect is not represented in any other public method however it is strongly connected to its original class and would be too complicated to refactor the code. More on the topic of testing constructors later.

Test Doubles

They are almost all just mock objects, but we distinguish them by use cases.

Dummy

A Dummy object just fills up a required parameter, but is never actually used.


$dummy = $this->createMock(SomeClass::class); $sut->action($dummy);


Stub

A Stub object is made to respond with a certain return value, but maybe will not be called at all.


$stub = $this->createMock(SomeClass::class); $stub->method('getSomething') ->willReturn('foo'); $sut->action($stub);


Mock

A Mock object is made to test method invocation times. The method is able to, but usually does not return anything.


$mock = $this->createMock(SomeClass::class); $mock->expects($this->once()) ->method('doSomething') ->with('bar'); $sut->action($mock);


Matchers from $mock->expects(matcher):

  • any() returns a matcher that matches when the method it is evaluated for is executed zero or more times.
  • never() returns a matcher that matches when the method it is evaluated for is never executed.
  • atLeastOnce() returns a matcher that matches when the method it is evaluated for is executed at least once.
  • once() returns a matcher that matches when the method it is evaluated for is executed exactly once.
  • exactly(int $count) returns a matcher that matches when the method it is evaluated for is executed exactly $count times.
  • at(int $index) returns a matcher that matches when the method it is evaluated for is invoked at the given $index.

Fake

A Fake object actually have working implementation, but usually takes some shortcut which makes it not suitable for production. ie.: InMemoryDatabase

Spy

A Spy object spies on a method call in a Mock object. Usually spies on arguments, invocation times, return value.


$mock = $this->getMock(SomeClass::class); $mock->expects($spy = $this->any()) ->method('spyOn'); $mock->spyOn("foo"); $invocations = $spy->getInvocations(); $this->assertEquals(1, count($invocations)); // we can easily check specific arguments too $last = end($invocations); $this->assertEquals("foo", $last->getParameters()[0]);

Spy's Invocation's methods are:

  • generateReturnValue()
  • getClassName()
  • getMethodName()
  • getParameters()
  • getReturnType()
  • isReturnTypeNullable()

PHPUnit in practice

Exceptions

We throw an Exception when a situation is expected to happen but has no sense. With an Exception we quit the normal flow of the application with reason so it should be tested.


// Call at the beginning of the test method! $this->expectException(InvalidArgumentException::class);

It is possible but I don't think asserting an Exception message or Exception code (ie. 1234) is a good practice.

Testing constructors in regular and abstract classes

Not recommended but shit happens.


class SomeClass { protected $someVariable; public function __construct($someParam = 2) { $this->setSomeVariable($someParam); } public function setSomeVariable($someVariable) { $this->someVariable = $someVariable; } }

A constructor is a magic method so disableOriginalConstructor() can be used to disable the call to the original class constructor to set expectations for our mock.

Sometimes not necessary but it is a good pattern to always provide the list of methods being used with setMethods().


// Get mock, without the constructor being called $mock = $this->getMockBuilder(SomeClass:class) ->disableOriginalConstructor() ->setMethods(array('setSomeVariable')) ->getMock(); // set expectations for constructor calls $mock->expects($this->once()) ->method('setSomeVariable') ->with($this->equalTo(5)); // now call the constructor $mock->__construct(5);

When dealing with abstract classes, mocking needs some more attention. Now it is necessary to provide the methods being used.


// Get mock, without the constructor being called $mock = $this->getMockBuilder(SomeAbstractClass:class) ->disableOriginalConstructor() ->setMethods(array('setSomeVariable')) ->getMockForAbstractClass(); // set expectations for constructor calls $mock->expects($this->once()) ->method('setSomeVariable') ->with($this->equalTo(5)); // now call the constructor $reflectedClass = new ReflectionClass(SomeAbstractClass:class); $constructor = $reflectedClass->getConstructor(); $constructor->invoke($mock, 5);

Docker cheat sheet command to quickly test a PHP project

docker run --rm --interactive --tty --user $(id -u):$(id -g) -v $PWD:/app -v ${COMPOSER_HOME:-$HOME/.composer}:/tmp composer update

docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp php:7.4-cli php vendor/bin/phpunit