Enhance PHP Unit Testing And Mocking Framework improve the quality

About Unit Testing

If you are new to unit testing and want to know what it is all about, this is definitely the place for you. We talk about the basics of unit testing and some of the widely held principles, including test-driven development.

We will also add some book recommendations if you want to learn more!

What is a unit test?

A unit test is a fast, automatic and repeatable way of proving your code does what it is supposed to do. It confirms that your code still works after you have made changes, or alerts you early on to the fact that something has broken. By writing unit tests that cover all of our code, we can quickly run them and check our entire application in a few seconds.

A unit test even acts as a piece of documentation as it describes how your code should be used.

Why would you write tests?

Before the development community was convinced of the benefits, some people thought that unit tests were a waste of time. Some developers thought that their job was to write production code, so spending time on unit tests, which aren't production code, wasn't what they were being paid for.

These days, most professional developers know that unit tests are an important part of writing production code. It means you deliver better quality code and it makes jobs like refactoring or adding new features easier.

The research team found was that the TDD teams produced code that was 60 to 90 percent better in terms of defect density than non-TDD teams - via Phil Haack.

If your code isn't covered by unit tests, it is legacy code.

The difference between unit and integration tests

A unit test isolates a small part of your code for testing purposes. Components that your code depends on are replaced with fake versions to make your test more stable. An integration test covers many components in a single test.

Usually a unit test calls a single method in a single class and tests a single piece of logic in that method, whereas an integration test ensures that two components interact as expected.

You should have as much unit test coverage as possible to ensure that the behaviour of your code is what you expect it to be. Integration test coverage should not test the detailed behaviour of the code (as this is already covered by unit tests) but instead focus on proving that components are present, connected and interact.

You can perform unit testing and integration testing with Enhance PHP.

Good practices

A test should only test a single logical unit. If you are making multiple assertions in a test, you should consider whether there should be multiple tests with a single assertion instead. If one test fails, it should be clear exactly what logic is broken; if you test too much in a single test it is hard to discern the cause of the failure.

Public Methods:

You should only test public methods. If the public method calls private methods, you can design your tests to exercise the different parts of logic contained in the private methods by calling the public methods with the right combination of arguments.

Dependencies:

If the class or function you are testing relies on some other component, you should supply a fake version of that component in order to isolate your test. You don't want changes in another component to affect your tests.

Naming:

The name of your test should be descriptive enough to tell you what has gone wrong if it fails. For example:

[MethodName]With[Condition]Expect[Outcome]

A real example would be:

AddTwoNumbersWith3And5Expect8

Some people use underscores instead of the "With" "Expect" syntax.

[MethodName]_[Condition]_[Outcome]

An example test

public function addTwoNumbersWith3and2Expect5() {
	$target = \Enhance\Core::getCodeCoverageWrapper('ExampleClass');
	$result = $target->addTwoNumbers(3, 2);
	\Enhance\Assert::areIdentical(5, $result);
}

This test is for a method called "addTwoNumbers", which is a simple addition function. We test it by passing in 3 and 2, and by proving that the response is 5. If the method gave back any other value the test would fail.

What are fakes, mocks and stubs?

A "fake" is a dummy class or object that you use in place of a real one for the purposes of testing. For example, if you had some code that relied on a class called "TaxCalculator", you wouldn't want to use the real Tax Calculator in your tests as this would make your tests brittle. If the Tax Calculator changes, it could cause your tests to fail despite the fact the code and behaviour of your code under test being correct. So instead of using the real Tax Calculator, which currently calculates tax at 22%, but next year may be 20% or 25% we create a fake one that always uses the same percentage. Now our tests won't be affected by changes in the tax calculator and will be less brittle.

Mocks and stubs are the two types of "fake" used in unit testing. A stub simply acts as a stand-in to allow the tests to run, whereas a mock is used to verify that certain calls with made with particular arguments. So a stub is just there to make your tests happen without relying on outside classes and a mock is used to pass or fail a test (i.e. if your code doesn't call a method it should call, or if it calls a method it shouldn't have called, you can fail the test).

You can hand-crank either a mock or a stub by writing a class with the same interface as the real class, and hard coding the return values. However, to save you the trouble, Enhance PHP will generate them for you.

How to use a mock

Create a mock and add an expectation:

$mock = \Enhance\MockFactory::createMock('ExampleClass');
$mock->addExpectation(\Enhance\Expect::method('addTwoNumbers')->with(3, 5)->returns(8)->times(1));

Pass it in to the class as a dependency:

$myClass = new MyClass($mock);

Verify the method was called the correct number of times with the correct arguments.

$mock->verifyExpectations();

How to use a stub

Create a stub and add any required return values:

$stub= \Enhance\MockFactory::createStub('ExampleClass');

$stub->addExpectation(\Enhance\Expect::method('addTwoNumbers')->with(3, 5)->returns(8));

Pass it in to the class as a dependency:

$myClass = new MyClass($stub);

You don't verify the expectations on a stub - it is just there to help you avoid testing lots of dependencies of a class when you are actually trying to test the class itself.
What is Test-Driven Development (TDD)?

Test-Driven Development is the process of only writing production code once you have a failing test. You first write the test to verify a single part of the functionality of your application, then you write the minimum amount of code to make it work.

If TDD takes 35% longer, it more than pays for itself. "defects found 'in the field' cost 50-200 times as much to correct" - via Phil Haack.

Here is a quick example...

Stage 1

First of all, we create our simple additions class, we are going to write a function that adds two numbers, taking two arguments that represent the numbers that should be added together. Before we even think about implementing the code, we write a test that we expect to fail. This is the complete example:

<?php
// Example built using TDD
include('../EnhanceTestFramework.php');
class ExampleClass {
	public function addTwoNumbers($a, $b) {
	}
}
class ExampleTestFixture extends \Enhance\TestFixture {
	private $target;
	
	public function setUp() {
		$this->Target = \Enhance\Core::getCodeCoverageWrapper('ExampleClass');
	}
	
	public function addTwoNumbersWith3and2Expect5() {
		$result = $this->Target->addTwoNumbers(3, 2);
		\Enhance\Assert::areIdentical(5, $result);
	}
}
\Enhance\Core::runTests();
?>

From this, we get the following result - because the method doesn't return anything.

addTwoNumbersWith3and2Expect5 - Failed
Expected 5 but was

Stage 2

Our next stage is to implement the minimum amount of code to make our test pass, so we change our method under test from:

	public function addTwoNumbers($a, $b) {
		
	}

To:

	public function addTwoNumbers($a, $b) {
		return 5;
	}

When we re-run our test, it passes:

addTwoNumbersWith3and2Expect5 - Passed

At first, it might seem strange writing such a small amount of code to make the test pass, but the point is that if we had written more code than this, the additional code we wrote over and above the minimum required is not actually fully tested.

By writing such a small amount of code to make the test pass, we force ourselves to write the next test to expose the next flaw in the function we are testing.

Stage 3

So because we want to write more production code, we need to expose a flaw in what we have using a test. This is the test we might add:

	public function addTwoNumbersWith4and2Expect6() {
		$result = $this->target->addTwoNumbers(4, 2);
		\Enhance\Assert::areIdentical(6, $result);
	}

Because we previously hard coded the return value to pass the previous test, this test will fail:

addTwoNumbersWith4and2Expect6 - Failed
Expected 6 but was 5

Stage 4

And we make this test pass by writing the minimum amount of code once again - only this time we need both our new test and the previous test to pass, so instead of changing the hard-coded return value to 6, we do things like this:

	public function addTwoNumbers($a, $b) {
		return $a + $b;
	}

addTwoNumbersWith3and2Expect5 - Passed
addTwoNumbersWith4and2Expect6 - Passed

Stage 5

We repeat this process until we have developer our entire feature. We can have high confidence that our tests are exercising all of the logical paths through our code and we can run our tests each time we make a change to ensure that we haven't affected the behaviour of our application.

The final point to remember with test-driven development is that if a bug is reported, you should write a test that exposes the bug before you fix it. At first your motivation for this process would be that if you have a test that exposes the bug, you can be certain the exact same bug will never occur again - because your test would fail and you would spot it before it gets released to a customer. Eventually, you will follow this process because it is actually easier to find the cause of a bug if you write a unit test that exposes it.