Learn the fundamental aspects of Test Doubles. Test Doubles are a way of dealing with the dependencies in your software to enable unit testing.
- View Offline
[Narrator] Here we'll begin our exploration of the basics of what a test double is. A test double, as defined by Martin Fowler, is a generic term for any case where you replace a production object for testing purposes. In essence, it means we swap out entirely, or a piece of our normal application code for an object that acts as that particular object. So, why would we ever need a test double? And what would be a purpose of an object when testing where we aren't actually using the real object? Well, the reasons are varied, but all fall back on one of the primary principles in unit testing.
Recall, one of the primary goals in unit testing is to test our code in isolation. Our test doubles replace portions of our 'in-use' dependencies, or inputs, to ensure we can test our code in isolation and focus on just the single unit or method at hand. The three main reasons we would want to use a test double is to replace a dependency, ensure some condition occurs, and, finally, to improve the performance of our tests.
We can see basically all of these in this simple example. We have a dependency upon the Model layer that we want to ensure we don't actually need to call and worry about what the Model layer is doing. Instead, we just need the Model layer to respond to our data and return something that matches its public API. Next, we would want to ensure that we return both a failure and success in validation, and then the Save operation. And finally, if we were building integration-style tests where we actually run the code for our external classes, Model and Log, we want to ensure they both actually occur as quickly as we can make them, so we aren't waiting on the file system.
We could have, for instance, the ability to write to an in-memory system. Think of it this way. Our tests should be fast, isolated, and have complete coverage, i.e., every line of code has some test executing it. When someone is talking about a test double, they're referring to the generic term for a variation of one of these five different objects. There are a dummy, fake, stub, spy, and mock. Each of these is designed to solve different variations of our three main reasons for using a test double.
A test dummy, which is the industry term for an object that we use, typically, as input to a method, that has not bearing on our actual test, but is required for our method to run. In the PHP world, this might mean a default instance of some object, or an empty array, or something else that we don't care what form it takes, just so long as our method will use it. Take a look at this example of a constructor method for a class that takes a Database instance, an ArrayObject that contains configuration settings.
If we are testing the case where the database is not connected and want to ensure the exception is correctly thrown, we can pass an empty ArrayObject to the constructor. After all, we don't care what happens to it. We only wind up executing two lines of this method and neither uses that object. A fake is an object in which we use it to build the simplified version of the object under control, typically, to achieve either speed improvements or to eliminate side effects. So here's one of the more common examples of this we might want to use.
In this case, our Database instance needs to actually run the SQL and return the result from it, but writing to the file system, or an external database server is really slow. Instead, we can swap it for a fake that uses an in-memory instance of the database for us speeding up our test without needing to fully duplicate the API for the database object. A stub is one of the more common test doubles. It simply provides a preset, or canned, answer to a method called on itself.
Typically, they require to be explicitly programmed for the exact methods being called, although they don't care how they are called, or with what inputs. Let's look at this example again. We could pass a stub for the Database instance to assure that when we call the isConnected method, it always returns false. After all, in our tests for the exception, that's all that we need. We don't care about anything else of our test, most likely. A spy is basically a higher level version of the stub.
Rather than just responding to inputs, it also provides information into how it was used, perhaps recording the number of times it was called. Here we need to send a collection of messages and we don't care what our test double does, but we do care how many times it was called. That calls for a spy. We can ensure that if we pass five messages, the spy send method is called five times and not ten, that's an easy win when we use a spy. The mock is one of the most common test doubles you will wind up creating for the simple reason they are the most capable of all test doubles.
Rather then blindly providing some functionality, they provide the ability to both pass a response, given a set of inputs. They can also tell you when you called a method on this test double that the mock isn't programmed for. Take our example of this spy. What happens if we want to improve this to ensure our message being passed to the email mock is ready to send a message? And, we possible have an instance of an invalid message. Well, a mock would let us test all of this in isolation since we can create a populated mock of the message class with a specific mocked instance of the message class to ensure it is correctly processed.
Test doubles are core to unit testing. The ability to test our code in isolation is vital to anything we build or create. However, don't be upset if someone gets the exact terminology wrong on what type of test double they are creating. The exact nuisances between each can get blurred pretty easily in complex testing scenarios. One of the most important themes, though, is to note what problem you are attempting to solve and focus on building a double that meets that requirement.
- Why use unit testing?
- Writing unit tests
- Extending unit tests
- Filtering PHPUnit tests
- Building dummy objects
- Working with data providers
- Writing an exception-based test
- Using TDD tactics
- Using PHPUnit advanced tactics, such as database tests