Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How to Effectively Test in PHP

DZone 's Guide to

How to Effectively Test in PHP

Let's discuss how to write effective tests with a walkthrough of a PHP test suite.

Free Resource

Any software has an unlimited amount of bugs. How can we make sure our system has as few bugs as possible and is operating as expected? Ideally, using debugger tools is an option, to easily define some breaking point and check system states to debug. Also, any developer can just go through code and debug. Then why have debugging tools been invented? In a typical system, there would be different types of bugs (such as library bugs, OS bugs, concurrency bugs, etc.); debugger tools are great tools to diagnose them; albeit they are not for debugging code. If you want to debug your code, you should write unit tests. I do not mean that a system with high test coverage has no bugs. In fact, tests are not just for identifying bugs.

Why Tests Are Very Important

Understanding the true definition of software testing can make a profound difference in the success of your efforts. Software testing is a process designed to make sure code does what it was designed to do and that it does not do anything unintended. Tests help us with two things: confidence and understanding. Software should be predictable and consistent, presenting no surprises for users.

We write tests to safely change and refactor every piece of code. Nobody is interested in trying to run the whole application until it is finished. We have seen projects that spend at most a few minutes in a state where their application is not working with the latest changes. The difference is the use of continuous integration. Continuous integration requires that every time somebody commits a change, the entire application is built and a comprehensive set of automated tests is run against it. We do not test for testing's sake, we do not test to make us happy - we do tests to see if our code works. As Kent Beck said, "I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence."

I heard that a business owner says, "We don't have the budget for testing" or "We don't have time to write tests." In fact, testing is not a product; it's a tool. You use tests to develop a product faster and better. How can someone say he doesn't have time to use the tool that makes the work faster? Also, a test is a safeguard against third-party code. Even if you do believe your code is working properly, then how do you ensure the behavior of other systems won't change over time?

When we write tests, we need to think about not just removing the problems of the code base but identifying other quality problems, such as design departures from requirements or usability problems.

How We Should Write Tests

The important consideration in a system under test is the design and creation of effective test suites. There is an argument that writing tests leads to better design. Of course, your test can become a good indicator to tell you there's something wrong. However, as I mentioned earlier, the main reason for writing a test is confidence and understanding of the system under test. It means each test should at least promote confidence and understanding. Tests should be purposeful, regulated, and well structured. Otherwise, they will cost too much.

One of the important aspects of each test is catching regression bugs. Seeing a green bar in a console does not mean your system is well tested. Tests are valuable when they catch a regression bug and at least contain business logic. Trivial code is not worth testing.

The other point is related to the way the test verifies the correctness of the system under test. Some tests have a false positive and false negative. Like reporting a non-existent bug, showing no faults even though there is a bug is non-determinism. Non-determinism refers to tests that sometimes pass and sometimes fail, without any noticeable change in the code, tests, or environment. These kinds of tests have a devastating effect on the system under test. Martin Fowler emphasizes that "non-determinism in tests is a virulent infection that can completely ruin your entire test suite."

The only way to reduce the chance of these problems is decoupling your tests from the system's implementation details as much as possible. Basically, the only thing we need to make sure of is the behavior, not each step. Without such decoupling, you inevitably end up getting failed tests in each refactoring, regardless of whether you break something or not. Let's see a real-world example of how testing implementation details can hurt:

class Hotel
{
    public function addCustomer(string $name, string $lastName)
    {
        $myList = ['fake name', 'fake last name'];
        $jsonified = json_encode($myList);
        $storage . customer($jsonified);
    }

    public function getCustomer(string $name)
    {
        $customer = $storage . getCustomerByName($name);
        $jsonified = json_decode($name);
        return $jsonified;
    }
}

and tests:

class HotelTest
{
    /** @test */
    public function should_return_right_customer()
    {
        $hotel = new Hotel();
        $storage = new StorageTest();
        $hotel->addCustomer('fake name', 'fake last name');
        $customer = $storage->getCustomerByName('fake name');
        $myList = ['fake name', 'fake last name'];
        $jsonified = json_encode($myList);
        $this->assertEquals($customer, $jsonified);
    }
}

The test is wrong because if you deciede to do not store your data as JSON, your test will fail. Instead, you should check the result like this:

class HotelTest
{
    /** @test */
    public function should_return_right_customer()
    {
        $hotel = new Hotel();
        $storage = new StorageTest();
        $hotel->addCustomer('fake name', 'fake last name');
        $customer = $storage->getCustomerByName('fake name');
        $this->assertEquals($customer->name(), 'fake name');
        $this->assertEquals($customer->lastName(), 'fake last name');
    }
}

As you see, we shift our focus from the "how" of the system under test to its "whats" and verify the end result.

What Is a Unit Test?

In the simplest terms, a unit test is a test designed to test one unit of work. In this case, one unit of work means single responsibility testing. Indeed, a unit test must have the cyclomatic complexity of 1. Unit tests can be written in a variety of ways, but all unit tests share some common characteristics, such as isolation from other code and from other developers. You can focus your efforts on your unit of work. You don't need to know the details of the other players in the system. This makes writing the tests, and the resulting code, easier.

There are three major styles of unit testing. The first one is functional, where you feed an input and check what output results. Obviously, this kind of unit testing is good for parts of the system under test that don't generate side effects. This style has the best protection against false positives and false negatives. Inputs and outputs tend to change less frequently during refactorings. You can alter the internal implementation of the system under test completely and tests written in a functional way will not be non-deterministic as long as the signature of the method under test stays in place. In the functional style, the input value is immutable.

The second type is testing a state, where you verify that the code under test returns the right results. State verification is more prone to test problems, but is still good enough as long as we verify the system under test's state and don't try to analyze its content via reflection. This, of course, requires you as a developer to think carefully about the parts of the system you expose publicly. You shouldn't reveal its implementation details, as that would introduce a tight coupling between the tests and the system under test.

The last one is collaboration specification. Here, you focus on collaborations between the system under test and its dependencies. You check that all collaborators are invoked in the correct order and with the correct parameters. A collaborator either maintains its own internal state, which can change over time, or refers to an external dependency. Therefore, binding unit tests to the system's collaborations introduces coupling between the tests and the system's implementation details. Such coupling makes the tests produce a lot of false positives and false negatives because the collaboration pattern tends to change often during refactorings.

Other Types of Tests***

In this article, I just focus on unit testing, but there are other important types of testing. Integration tests are an important step in software development and should not be skipped or left until the end of development. It's a kind of test that determines if independently developed units of software work correctly when they are connected to each other (usually with the database and the file system). They can potentially interfere with each other through those dependencies and thus cannot run in parallel.

A stress test is another important type of test. It's an indicator of what is the maximum capacity of the system. Also, load testing is when you test how your application acts under anticipated load; for example, if you expect 500 concurrent users, it's the process of assessing your application under that load. Performance testing is a testing technique; it is not something you can apply to your web application directly. in the other hand, thread-safety testing is an important quality of classes in languages/platforms like Java, where we frequently share objects between threads.

Test Doubles

"Test double" is a generic term for any case where you replace a production object for testing purposes. We have different types of test double: dummy, fake, stub, spy, and mock. A mock is somewhat of a generic term that covers a family of stand-in objects for use in unit testing. Dummy objects are simple mocks that stand in for an external resource. They usually return a predefined response for a method when that method is invoked, but they usually can't vary that response based on the input parameters. An example of a dummy is:

class MyUnitTest
{
    /**@Test */
    public function testConcatenate()
    {
        $myUnit = new MyUnitDummy();
        $result = $myUnit->concatenate("one", "two");
        $this->assertEquals("onetwo", $result);
    }
}

class MyUnitDummy
{
    public function concatenate(string $paramOne, string $paramTwo)
    {
        return "onetwo";
    }
} 

On the other hand, stub and fake are far from dummy objects in that they can vary their response based on input parameters. The main difference between them is that a fake is closer to a real-world implementation than a stub. Stubs contain hard-coded responses to an expected request. Let's see an example:

class HotelTest
{
    /** @test */
    public function testConcatenate()
    {
        $stubDependency = new StubDependency();
        $result = $stubDependency->toNumber("one", "two");
        $this->assertEquals("onetwo", $result);
    }

}

class StubDependency
{
    public function toNumber(string $param)
    {
        if ($param == "one") {
            return 1;
        }
        if ($param == "two") {
            return 2;
        }
    }
}

A mock is a step up from fakes and stubs. Mocks provide the same functionality as stubs but they are more complex. They can have rules defined for them that dictate in what order methods on their API must be called. Most mocks can track how many times a method was called and can react based on that information. Mocks generally know the context of each call and can react differently in different situations. Because of this, mocks require some knowledge of the class they are mocking. A stub generally cannot track how many times a method was called or in what order a sequence of methods was called. A mock looks like this:

class MockADependency
{
    private $shouldCallTwice;
    private $shouldCallAtEnd;
    private $shouldCallFirst;

    public function StringToInteger(string $s)
    {
        if ($s == "abc") {
            return 1;
        }

        if ($s == "xyz") {
            return 2;
        }
        return 0;
    }

    public function ShouldCallFirst()
    {
        if (($this->shouldCallTwice > 0) || $this->shouldCallAtEnd) {
            throw new AssertionException("ShouldCallFirst not first thod called");
        }
        $this->shouldCallFirst = true;
    }

    public function ShouldCallTwice(string $s)
    {
        if (!$this->shouldCallFirst) {
            throw new AssertionException("ShouldCallTwice called before ShouldCallFirst");
        }

        if ($this->shouldCallAtEnd) {
            throw new AssertionException("ShouldCallTwice called after ShouldCallAtEnd");
        }
        if ($this->shouldCallTwice >= 2) {
            throw new AssertionException("ShouldCallTwice called more than twice");
        }
        $this->shouldCallTwice++;
        return $this->StringToInteger($s);
    }

    public function ShouldCallAtEnd()
    {
        if (!$this->shouldCallFirst) {
            throw new AssertionException("ShouldCallAtEnd called before ShouldCallFirst");
        }
        if ($this->shouldCallTwice != 2) {
            throw new AssertionException("ShouldCallTwice not called twice");
        }
        $this->shouldCallAtEnd = true;
    }

}

Finally, a Spy is a stub that also records some information based on how they were called.

Using mocks is not a good option. The problem with mocks, in general, is that they aim at verifying collaborations and often do that in a way that ties unit tests to the implementation details of the system under test. Another problem with mocks is that they encourage destructive decoupling-induced design damage.

Testing in Domain-Driven Design

In domain driven design, we should separate dependencies that work with the outside world and domain model (where we put all the domain logic). Indeed, our code should either depend on the outside world, or represent business logic, but never both. Mocking business logic just doesn't make any sense because both the mock and the original object have predictable behavior, which doesn't change because of external factors. Also, if you extract an interface out of a domain model in order to enable unit testing, it is a design smell. Because they use header Interfaces that, they have the only implementation and no use of abstraction.

Conclusion

  • Tests help us with two things: confidence and understanding.
  • A unit test must have the cyclomatic complexity of 1.
  • Important aspects of each test are catching a regression bug, preventing a false positive and false negative, and fast feedback.
  • There are three major styles of unit testing: functional, state, and collaboration-specification.
  • In DDD, our code should either depend on the outside world or represent business logic, but never both.
Topics:
unit testing ,php ,mocking ,performance ,testing ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}