In my last article, in this series on advanced object-oriented programming (OOP) for PHP WordPress development, I showed how to setup the WordPress test suite for integration tests. Previously, I had written about unit tests, which I showed how to run in an isolated environment. These tests were not dependent on WordPress at all, so they could just run on the system’s PHP.
I don’t want to learn all that. Instead, I want to go to the plugin I have and type one command to install the environment — composer wp-install — and one to run the integration tests — composer wp-tests. That’s all setup, now it’s time to talk about how to write these tests. Specifically, let’s look at how the assumptions we are willing to make are different when writing integration tests as opposed to unit tests.
Reversing Our Assumptions
In my article about unit testing, I talked about how some of the tests were based on the assumption that WordPress would work properly. Based on that assumption, I wrote tests that relied on mock data. Those assumptions allow those tests to prove that the code’s public API is consistent and that the core function — what is done with the input — works.The weakness of the assumption unit tests are based on, and implicated on a practical level by isolating out all side effects — is we’re assuming the connection to WordPress is properly connected, will have consistent output and the consistent output will produce the right effect on WordPress. Integration tests make the opposite assumption — we assume our code is working in isolation and we test its effects without isolation.
If we were making sinks, we’d write unit tests to make sure faucets turned on and off and produced hot or cold water as directed and then we’d write an integration test to make sure the water went into the sink instead of just pouring out onto the floor, or some of it going in the sink and some of it leaking out of the pipe too soon.
Writing The Tests
What Do We Test?
The code we’ve created so far affects WordPress in two ways — it adds a filter that results in different WP_Query and REST API results. So, we need to test that the effect of this code is the filter was added in the way we wanted it to be as well as that in the right situations WP_Query and REST API results are modified the way we expect and that there are no effects when there should not be.
Testing Adding and Removing Filters
For this first group of tests, we’re just looking to prove that our code can add and remove hooks properly. The class FilterWPQuery has a method addFilter() and a method called removeFilter(). We can cover those interactions by testing that addFilter() causes WordPress to report that the filter was added. We can also test that removeFilter() causes WordPress to report that the filter was removed.
Other tests we have cover what happens if the filter is added. WordPress core has tests to cover hooks. This is is enough for covering this effect.
To test that the hook is added, we’ll instantiate the class and use it to add the filter and then use PHPUnit to assert that the result of has_filter() is what we would expect if the filter was added:
To test that the hook can be removed, we’ll instantiate the class and use it to add and then remove the filter and then use PHPUnit to assert that the result of has_filter() is what we would expect if the filter was added and then removed:
Those two tests prove that we can effect WordPress. Before we move on to testing how we effect WordPress, is to test that the filters do not have unintended side effects. We’re testing a system that sometimes modifies what posts WP_Query will return, we need to make sure our system doesn’t always modify what posts WP_Query returns.
Our system is designed to modify WP_Query, when it is used by a specific REST API route. Later, we’ll test that route. For now, we’ll add our filter and then insert one post into the database, then check if its the post that a WP_Query for all posts returns. That’s what it’s supposed to do by default, let’s make sure it still does this when we add our filter.
In this test, we use the post factory from the WordPress test suite to create the post. A class that implements the factory pattern, which I covered in a previous Torque post, is a class that creates objects of other classes.
One great use for the factory pattern is creating test data. By having a single, standard method for creating a type of test data — in this case WordPress posts — decreases code repetition and ensures we can trust our test data. It also makes it easier to write tests.
The post that is created with the post factory is what our tests are asserting the result of the query is the same as. The factory creates our expected result, that our tests compare against. We do not need to test every part of the resulting post, WordPress core’s tests covers WP_Post.
Testing WP_Query Results
We have not yet gotten to the point where the WP_Query results are anything but mock data. We want tests that prove that the whole system is reliable, by the standard of it always outputs an array of WP_Post objects that is the right size.
Later on we can change what happens internally, don’t worry we are almost to that article. When we do, the new code will need new unit tests. But we will not need new integrations tests. In fact, when we implement a different system for running the search, for example ElasticSearch or SearchWP, we should make it a requirement that these integration tests pass without any modification.
All of this functionality is encapsulated in the getPosts() method. So the two tests we need to cover that method are one to prove it returns an array and another that the array has the right WP_Posts.
Testing that that getPosts() returns an array is simple. We write a test to assert that the result of the function that is_array() when passed the results of getPosts() is true. We’re proving it is an array:
That test covered what the type of results — the results of the type of array. The next test covers the contents of the results — the array contains WP_Posts. For this, we’re going to use getPosts() to get the array of posts and loop through the results, asserting that each one is a WP_Post:
Testing REST API Routes
All of those tests prove that WP_Query would produce the results we expect it to, when we tell it we are in the right context. That’s good, but it doesn’t prove that the results will be correct in that context. Now it’s time to test the effect our code has on the REST API responses that it is designed to modify.
I have covered testing REST API endpoints before for Torque. That article covered custom endpoints. These next tests cover modified default endpoints. But the way we test is similar. We’re not going to make an actual HTTP request — that would get us into acceptance testing. We’re going to use a mock WP_REST_Server, the same one WordPress core tests use.
We have to trust the assumption that WordPress mock WP_REST_Server accurately reproduces the behaviour of the WordPress REST API. That assumption can be made because of WordPress core’s test suite. If you find a problem with WordPress’ tests that cover the WordPress REST API, then you should open an issue in core trac and submit a patch.
We’ll need two tests. One to test that that the conditional logic in our system, which should return true in this context. The second test will prove that the results are the results our system should generate.
Here is our first test, it dispatches the request and asserts that the shouldFilter() return true:
That proves that the system knows it should filter the results. Now let’s prove the contents of the result set are correct. This is very similar to the tests of the WP_Query results. We are just checking that we see the right WordPress posts resulting from the REST API being used in its natural way. We’re testing just the effect of our system.
This test starts the same as the last one, by creating a mock request. This time we loop through the results and compare them to the output of the method getPosts(). That’s exactly the same method that is being used underneath. They should produce the same results. If something was wrong with our code, this test would be comparing those expected results to a WP_Error object or some other set of results and therefore the tests would fail.
What’s cool about this pattern, is that when the getPosts() method changes, so does the data the tests is comparing against. This test doesn’t check for any specific data — other tests can do that. It just makes sure these two different ways of accessing the same data stay in sync.
That may seem like a small point. It’s not because previous to these tests, we have written or identified tests that make it safe for this test to make a small number of assertions and cover a lot of functionality of the system, most of which we don’t control.
This last test becomes the ultimate test of our system. It’s the final result. But it’s no different than every other test. Each one proves one small thing and assumes every other test is passing. Testing is all about safe assumptions. Once one test fails, all assumptions are untrusted.
In the next article in this series, I will cover setting up Travis CI to run the tests, in multiple environments on all pull requests created in the Github repo for the plugin. This kind of testing automation, combined with a git-flow workflow will enforce that in the future all changes to the code require all of the tests to pass. We call this testing automation continuous integration or CI for short.
Thinking About Assumptions
My strategy for unit and integration testing, and using a different environment for each type of testing does rely on some assumptions that are worth questioning. For example, there are very complete mocking frameworks like WP_Mock and Mockery that could provide reliable enough data that I could test my integration with WordPress without having a WordPress site.
I’m going this way because I think it’s easier to teach and understanding how to use phpunit for isolating unit testing is a precursor to being able to use it with other tools. Using phpunit with the WordPress test suite, like we just did was a practical example of another tool. Many of the other tools we might want to work with, for example, Behat, a PHP behavior-driven testing framework that we can use with WordPress thanks to WordHat, require phpunit and local test environment like this series is using for integration tests.
I hope that you’ve seen what assumptions we had to make in this article and can look at the benefits and weaknesses of the different approaches as you evaluate which of these testing tools are best for your workflow.