In this article we are going to look at the actually unit tests for the TruUcde plugin code. It is worth reading the previous posts in this series before tackling this one. They cover the code we’ll be testing, concepts and tooling including setup. It may be particularly useful to review the first article as this is the code we’ll be testing here.
Types of tests
I know I briefly mentioned this in a previous post, but it is useful to review the different types of testing and their roles. Different people understand these types of testing differently, this is my blog post, so this is the way I understand them. The other thing worth noting is that there is universal agreement that you only need to test the code you’ve written. The assumption is that outside code is assumed to be sound and already tested. (What could possibly go wrong.)
We are going to look at the first two types of testing in this series and leave the other two for a future series. However, if you want some fun, find a group of developers in a pub and slap my definitions down and ask them what’s wrong with them. It will easily be as exciting as a vim vs. emacs debate. In this article we will discuss unit tests only.
What to test
It is also worth reviewing the AAA model of testing: Arrange, Act and Assert. In the arrange part of the cycle we set up our context, in the act part we run our code under test and in the assert part we test to see if the results meeting our expectations.
In this example my plugin has 4 functions, although it began its life as one big long procedural ramble. In order to test, you need something in the code under test to call from test file. This mean functions or class methods. So, I could have had this code all in one big function but it is much more difficult to test. Here’s why.
Booleans and flow logic kill you. The more complex your booleans and flow logic the more variations you have to keep track of and test. Let’s look at an example.
So, let’s take an if…then statement with a boolean conditional. For example, if A and B then do something. Each of A and B can be true (T) or false (F). When we add up the possible combinations we have 4. (TT, TF, FT, FF). The resulting if statement is T with TT and F otherwise. This is the case with my user_can_add() function.
Now, let’s take another example. A & (B or C). This gives us 8 combinations: 3 T and 5 F. This is the case with my e_needs_processing() function.
These two functions are what are sometimes called “guard functions” as they guard, with conditions, some following code against entry. What we need to test for is all the combinations of pathways through the code. With procedural code these two functions would be nested which multiplies their respective pathways for a total of 32, and 32 tests would be necessary to completely test this.
This is why I broke out these two functions, watch what happens. I still need another boolean if statement to tie these two functions together (it’s the first one in the on_loaded() function). This gives me another 4 pathways to test. In total this is 4 + 8 + 4 = 16, half as many tests and half the complexity. And also we get to deal with them in sets rather than a total of 32.
Okay, so down to brass tacks, I have 4 functions, so I need to test all 4 of them. Well, I don’t really have to, I could only test ones I’m worried about. But it’s like my Dentist once said to me, “You don’t have to floss all of your teeth. Just the ones you want to keep.”
So to recap the organization of my plugin, I’ve got two guard functions: user_can_add() and e_needs_processing(). The main engine function is on_loaded() which does the processing of the error object. The fourth function is truucde_init() whose sole function in life is to add on_loaded() to the wpmu_validate_user_signup WordPress filter.
Unit Test: Setup Review
The past few articles discussed tooling and installation. Here is the composer.json file for the project to kick off a brief review.
{
"name": "twelch/truucde-blog",
"description": "WordPress plugin for Super and Site Admins to add users without white/black list restrictions.",
"type": "wordpress-plugin",
"license": "GPL 2.0+",
"authors": [
{
"name": "Troy Welch",
"email": "twelch@tru.ca"
}
],
"require": {},
"require-dev": {
"squizlabs/php_codesniffer": "^3.5",
"wp-coding-standards/wpcs": "^2.3",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.6.2",
"roave/security-advisories": "dev-master",
"phpunit/phpunit": "^7",
"10up/wp_mock": "^0.4.2"
}
}
The first four items in the required-dev
section are related to code sniffing which checks code during the writing process for style, formatting and static syntax errors. Also there is a security plugin. The final two items are PHPUnit and WP Mock which we will use in this article for our unit testing.
On to the unit tests.
I’m going to work my way through my unit test file line by line and explain what is going on. There are a lot of little lessons I learned along the way that may be helpful, but I’ll probably skip most of the comment blocks. If they aren’t either pro forma or understandable on their own then I’ve done it wrong.
<?php
/**
* TruUcde
*
* @package Truucde
*/
namespace TruUcdeBlog;
require_once 'class-wp-error.php';
use \WP_Error; // need to create empty error objects.
Okay, as you can see the test file is a php file. The first real command on line 7 is a namespace definition. I’m not going to talk much about namespaces except that they are a good idea. Tom McFarlin has written a great post on namespaces and WordPress. All your php files for your project should be namespaced. It makes it easier to use things from other files, for example, I can call all the functions from my plugin file in my test file because they are part of the same namespace. Also, this protects against name collisions with other functions and classes when all of this stuff is running in a WordPress context. I’ll point out where it is used as we go along. Namespacing is also integral to auto-loading, also out of scope for this article series.
The next two lines (9 and 10) are probably the most controversial lines. The first line requires (loads) the WordPress error file and the second indicates that I wish to use the WP_Error
object in my tests. Technically this means that my unit tests are not fully insulated from outside code. However, I am not testing inside of a running WordPress environment so I’m mostly insulated. I talk about it a little in the first post of this series, but the use of this object and its class methods greatly simplifies my work. I’ll point it out as we come to it. You will see that I have more than twice as many lines of code in this test file than I do in the plugin file, so saving labour is important. As this plugin is filtering the WP_Error
object it makes a certain amount of sense to have it on hand for testing. Also, if I didn’t use the actual WP_Error
object I would have to mock it, and by the time I was done I would basically have the same thing.
One thing I have done is create a symbolic link in my testing directory to the error class file in my local WordPress install. This ensures that as I update WordPress the link points to the current version of the file, and my tests can find it via the symbolic link. (Note the \
in front of WP_Error
on line 9. Because we are in the TruUcdeBlog
namespace we need to use the slash to access WP_Error
which is in the global namespace. )
/**
* TruUcde test case
*/
class TruUcdeTest extends \WP_Mock\Tools\TestCase {
/*
* Define all the things
*/
public $Target_code = 'user_email';
public $Black_msg = 'You cannot use that email address to signup. We are having problems with them blocking some of our email. Please use another email provider.';
public $White_msg = 'Sorry, that email address is not allowed!';
public $Target_data = 'User email things.';
public $Another_code = 'user_foolishness';
public $Another_msg = 'The eagle lands at midnight';
public $Another_data = 'Spy games';
From here on in line numbers in my code samples will not match the line numbers in the original files. They will, however, save us from some cumbersome prose as I attempt to explain what I’ve done.
Okay, you’ll see the class declaration for my tests on line 4. Typically test files will extend some other class in order to make the class methods (testing commands) available. This one extends the WP_Mock TestCase which it makes it useful when mocking WordPress functions. If I was using PHPUnit without WP_Mock I would instead extend PHPUnit’s TestCase. In any event WP_Mock TestCase extends PHPUnit’s TestCase so I have the functionality of both.
Next are a bunch of variables, or attributes as they are called when they are inside of a class. These will save me a bunch of repetitive typing and will possible make the tests easier to read and understand.
/**
* Test setup
*/
public function setUp(): void {
\WP_Mock::setUp();
}
/**
* Test teardown
*/
public function tearDown(): void {
\WP_Mock::tearDown();
}
Test suites typically come with provision for setup
and teardown
methods which apply to various scopes. These are global in that they will apply to all tests. The basic cycle of a test is to setup the required test conditions, run the code to be tested, check the result, and then wipe the slate for the next test. See the discussion of the AAA model above: Arrange, Act, Assert.
In this case, most or all of my tests require WP_Mock
. This saves me from typing the WP Mock setup and teardown methods for each individual test. Note the notation of this command: \WP_Mock::setUp()
. The slash in front indicates that this class is in the global namespace rather than the TruUcdeBlog
namespace declared for this file above. (Very much like specifying the root directory of a disk in a full filename). WP_Mock
is the name of the class, the ::
notation indicates that it is a static method (static methods are class methods that can be run without instantiating a class) of the parent class of the TruUcdeTest class that we are in, and setUp()
is a class method.
Testing the truucde_init() function
Let’s look at the function first, then its test.
/**
* Hooking into WordPress
*/
function truucde_init() {
add_filter( 'wpmu_validate_user_signup', 'TruUcdeBlog\on_loaded', 10 );
};
Simple function. This simply adds the on_loaded
function to the wpmu_validate_user_signup
filter hook. Note the use of the namespace path for the on_loaded
function. This ensures that when WordPress calls our function that it is called from the correct namespace.
From a testing perspective, what we care about here is that the function is properly loaded on the filter hook.
/**
* Test that hooks are being initialized.
*/
public function test_it_adds_hooks(): void {
// ensure the filter is added.
\WP_Mock::expectFilterAdded(
'wpmu_validate_user_signup',
'TruUcdeBlog\on_loaded'
);
// Now test the init hook method of the class to check if the filter is added.
truucde_init();
$this->assertConditionsMet();
}
The code comments are pretty instructive here, but first remember that the WP_Mock setup() function has been run by our test setup function, otherwise we would need it here as well. WP_Mock provides some convenience methods for us, such as the expectFilterAdded() on line 7. What we are saying in lines 7-9 is that when we check (in line 13) we expect that the specified function has been added to the specified hook. So this takes care of the ‘arrange’ step of the AAA model.
Then we call our function from the plugin file on line 12. This is the ‘act’ step of the AAA model. No pathname is needed because it is in the same namespace as this file.
Then in line 13 we assert that the conditions specified in lines 7-9 should now be met. If they are, then the test passes. If they are not it fails. This is the ‘assert’ step of the AAA model. For the rest of our discussion I won’t specifically reference the AAA model, but it will still be underpinning what we are doing.
Now the teardown command will run and clear the mock.
Okay, that one was pretty easy, there wasn’t much we needed to test. Let’s move on.
Testing user_can_add()
Okay, let’s look at the actual plugin function first. It’s fairly simple, but the tests are a little more complex.
/**
* Tests if the current user is logged in and has admin/super admin privileges.
*
* @return bool
*/
function user_can_add(): bool {
if ( is_user_logged_in() && current_user_can( 'promote-users' ) ) {
return true;
} else {
return false;
}
}
Okay, as explored in the previous post, this guard function makes sure that the user is both logged in and has the permissions to ‘promote-users’ (is either a site admin or super admin). Also, as explored in my ramble about pathways above, there are 4 possible outcomes from running this function. is_user_logged_in()
and current_user_can
are both WordPress functions that will return either true or false. Our function here will only return true if both of the WordPress functions return true (case 1). If either returns false and the other returns true (cases 2 and 3), or they both return false (case 4) then our whole function returns false and the user will not have the on_loaded
function executed for them.
We have to test for all 4 of these cases if we want to be thorough. Let’s look at the first case.
/**
* Tests that user checks are working as they should
* TT
*/
public function test_user_can_add_TT() {
// Mock 'is_user_logged' in and 'current_user_can'.
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote-users' ),
'return' => true,
)
);
// Run it through the function.
$result = user_can_add();
// And assert
$this->assertSame( true, $result );
}
Note, I am writing a separate function for each of the four cases because the mocks change for each case. This enables fresh setup and teardowns for each case. Note that as I need unique function names for each of the 4 cases, I’ve appended TT, TF, FT and FF to the function name to correspond to the test case.
So, we have two things to mock this time. The WP_Mock::userFunction
method is a general use method for mocking WordPress functions. In the first one we are mocking is_user_logged_in()
and the array below indicates that it should only be called 1
‘times
‘ and it should return true
. In the second mock we are mocking current_user_can()
and its array also indicates that it should be called only once, also return true
, and that it has a function argument of promote-users
.
Then we run the function and store its output in the $results variable. And we have a test assertion, assertSame
, that tests that the two arguments presented to it are the same. In this case these are true
and what’s in the $result
variable.
I’m going to save a little space here, cases 2, 3, and 4 look much the same as the above except for lines 12, 20 and 26. Here’s a little table summarizing the cases:
Return value/Case | TT | TF | FT | FF |
---|---|---|---|---|
is_user_logged_in (line 12) | Return True | Return True | Return False | Return False |
current_user_can (line 20) | Return True | Return False | Return True | Return False |
assertSame (line 26) | ( true, $result ) | ( false, $result ) | ( false, $result ) | ( false, $result ) |
Obviously some time can be saved here by coding case 1, copy and pasting 3 more times, and changing the function names and lines according to the table. We’ll see some more time savings steps below, but also some dangers.
Testing e_needs_processing()
Okay, let’s have a look at this function.
/**
* Tests if an actual error object is passed, if it is empty and
* if it contains a black or white list error message. Returns true
* if error object needs processing
*
* @param \WP_Error $original_error Error object passed in the wpmu_validate_user_signup results.
*
* @return bool
*/
function e_needs_processing( \WP_Error $original_error ): bool {
if ( ! empty( $original_error->errors ) // is not empty.
&& ( // and contains a black/white list error.
in_array( BLACK_LIST_MSG, $original_error->get_error_messages( TARGET_ERROR_CODE ), true )
|| in_array( WHITE_LIST_MSG, $original_error->get_error_messages( TARGET_ERROR_CODE ), true )
)
) {
return true;
} else {
return false;
}
}
Okay, we have our other guard function here. This one tests a lot. We need to know whether to process the error object to remove white list and black list errors so that we can add our users. A note on formatting and line breaks. I attempted to make this function a little more readable by breaking the lines such that the A && (B || C) structure is more obvious.
The function tests for the following:
- a valid WP_Error object is passed to the function
- the error object is not empty
- the error object contains a black list error message, white list error message or both.
If all these conditions are met then our function will return true. There is quite a lot of work here to test all of these conditions, so I’m looking for shortcuts.
Shortcut 1: type hinting the function declaration. Don’t you just love it when I talk dirty? Look at line 10 above. The \WP_Error
in the parameter brackets is saying ‘don’t accept an argument that is not an instance of the \WP_Error
object’. This means that we don’t have to write a test for the first bullet point. We know anything coming in will be a valid error object.
Shortcut 2: We have a reasonably complex boolean in our if statement. The description of this is covered in the first post of this series, but it takes the following form: A and (B or C). So we know that any time A is empty the whole function will return false. We can take care of this (and bullet 2) with a fairly simple test.
/*
* Tests error object empty (4 cases)
*/
public function test_e_needs_processing_empty() {
// Is error object -> is empty (4 cases).
$empty_wp_error = new WP_Error();
$result = e_needs_processing( $empty_wp_error );
$this->assertFalse( $result );
}
Note in the code comments I refer to (4 cases), I’ll get back to that shortly. Let’s look at the test first.
Here is one of the places I wanted access to the real WordPress error class. I start by instantiating a new, empty, WP_Error object with the: new WP_Error()
command in line 6 and store it in a variable.
Then in another variable, $result,
I store the result of calling the e_needs_processing()
function with the empty WP_Error object as an argument. The if statement in the e_needs_processing function returns false if the error object is empty, so we don’t even have to look at the rest of the boolean in this case.
Our final test statement on line 8 confirms that the function returns false. Otherwise something is wrong and our test will fail.
Okay, let’s return to the (4 cases) in our comments and our earlier notes that there are 8 cases represented by this function. I’m going to build a little truth table.
Check/Case | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Non-empty error object | F | F | F | F | T | T | T | T |
Have blacklist msg | T | T | F | F | T | T | F | F |
Have whitelist msg | T | F | T | F | T | F | T | F |
Function returns | F | F | F | F | T | T | T | F |
Because the non-empty error object condition is on one side of a Boolean ‘and’ false (empty) for this condition in the test above, this takes care of the first 4 cases. In the remaining 4 cases only case 8 (no white or black messages to process) will return false for the function. Okay, let’s test it.
/*
* Tests truth table of list msg presence.
*/
public function test_e_needs_processing_lists() {
// Doing this the lazy way, adding and removing as I go
// to create the various truth table conditions
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
// FF
$result = e_needs_processing( $truucde_error );
$this->assertFalse( $result );
// TF
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$result = e_needs_processing( $truucde_error );
$this->assertTrue( $result );
// FT
$truucde_error->remove( $this->Target_code );
$truucde_error->add( $this->Target_code, $this->White_msg, $this->Target_data );
$result = e_needs_processing( $truucde_error );
$this->assertTrue( $result );
// TT
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$result = e_needs_processing( $truucde_error );
$this->assertTrue( $result );
}
I’ll run through this one in a minute. Have a look and see what is wrong with my approach, even though it works. There’s a hint in the comments.
Okay, what is wrong is largely philosophical, but what I should have done is split this into four functions one for each case. I may still need to do this if the project evolves. In general we don’t want to carry state from one test to another. Put another way, in the language of the AAA model, each different test should have its own arrangements. Or put even another way, if I inserted another test, or removed or rearranged these tests, the resulting state may no longer be correct.
For now it does work, so let’s go through it. First, I instantiate a new WP_Error
object. Then I add an error message that is not one of the ones we are targeting. This is added using the WP_Error
object class methods, which is another reason I kept the WP_Error
class. It is important that other error messages get through. Now we can test our FF scenario (#8 in the table above). We pass the error object to our function in line 12. This should return false and we test that assertion on line 13.
Next case #6. On line 16 I add in a Black list error message, run the error object through the function and assert true on lines 18 & 19.
Case # 7. Remove the Black list message (line 22), add the White list message (line 24), and rinse and repeat. (lines 27/28)
Case #5. Add back the Black list message (line 31) and rinse and repeat again. (lines 33/34) It is in this case that the problem of my approach shows. We have to know what the state of the error object is, that the white list error is already there when we add the black back.
Testing on_loaded()
Okay, we are on the last function, but it’s a doozy. First the function.
/**
* Checks user permission, existence of WP_Error object messages to remove,
* builds new array, returns $results object with new array in place of the
* original.
*
* @param result $result Return of wpmu_validate_user_signup.
*
* @return mixed
*/
function on_loaded( $result ) {
// get error object from wpmu_validate_user_signup result.
$original_error = $result['errors'];
// return original array if auth conditions not met,
// or black/white list messages don't exist.
if ( ! user_can_add() || ! e_needs_processing( $original_error ) ) {
return $result;
}
// create a new (empty) WP_Error() object for holding transferred entries.
$new_error = new WP_Error();
// Run through error codes and messages keep all but white/black list errors.
foreach ( $original_error->get_error_codes() as $code ) {
foreach ( $original_error->get_error_messages( $code ) as $message ) {
if ( TARGET_ERROR_CODE !== $code ) {
$new_error->add( $code, $message );
} else {
// Don't add the white/black list messages.
if ( BLACK_LIST_MSG !== $message && WHITE_LIST_MSG !== $message ) {
$new_error->add( $code, $message );
}
}
}
// add data entries for original to new error object.
if ( ! is_null( $original_error->get_error_data( $code ) ) ) {
$new_error->add_data( $original_error->get_error_data( $code ), $code );
}
}
// Put the new error object back in $result and return.
$result['errors'] = $new_error;
return $result;
};
Okay, this function is described in detail in the first post of this series, but there are some notes I’ll make here. On line 10 the result of WordPress’s wpmu_validate_user_signup
is passed into this function (remember, this function sits on the filter hook of the same name). The $result is an array, one entry of which is the WP_Error
object with the array key errors
, this is extracted and put into a variable on line 12.
On line 16 our guard functions are checked. if either of them doesn’t pass the function is exited on line 17 returning the original $results object back to the wpmu_validate_user_signup
WordPress function via the filter hook.
We then create a new WP_Error
object (line 21), run through the original one copying over all error messages and data except for any White list/Black list ones we are targeting (lines 24-39). Finally, we replace the original error object with the new one in $result
, return it to WordPress and exit the function.
Okay, so how to test. I’m interested in two things. First, it properly returns the original $result
if one or both of the guard functions are false. This is often called an ‘early return’ as return statement returns the original $result and drops out of the function without further processing. Second, if both guard functions are true, whether it properly processes and returns the expected WP_Error
object.
Let’s look at the bail tests (early return) first. For completeness we need three tests: one each for a false in one ofe_needs_processing()
or user_can_add()
and a true in the other, and one where they are both false.
We know from our previous tests how to reliably get a true or false return out of each of these functions. With user_can_add()
we mock the WordPress functions that it relies on and return values that create a false return for the function. With e_needs_processing()
we need to feed it a WP_Error
object with the appropriate errors. In these next tests the WP_Error()
object will get to the e_needs_processing() function via the on_loaded() function. It is important to remember that we don’t need to account for the all the different ways that these two functions can be made true or false. We’ve already tested this. Alls we are trying to do is generate the return value so that we can test on_loaded()
.
/**
* Testing if on load conditions failure returns
* original object
*/
// user_can_add() return true, e_needs_processing return false
public function test_on_load_conditions_bail_utef() {
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => true,
)
);
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
$orig_result = array();
$orig_result['errors'] = $truucde_error;
$result = on_loaded( $orig_result );
$this->assertEquals( $orig_result, $result );
}
}
There are a number of ways to approach this, but I chose a quasi-simulation approach. In lines 8 – 23 I mock the WordPress functions that drive the user_can_add()
function to generate a true return. I instantiate a new, empty WP_Error()
object and store it in $truucde_error
(line 25). To this I add a non-targeted error message which will generate a false return from e_needs_processing()
when it gets there.
Then I need to simulate a return from the WordPress hook, the $results array
. I create an empty array, store it in $orig_result
(line 29) and then store the prepared WP_Error
object ($truucde_error
) in the array with the correct array key of ['errors']
(line 30).
This gives me what I need to pass to the on_loaded()
function and store the result of that in $result
(line 32). Then in line 34 I test to see if the passed array is the same as the processed array. Note: we used assertSame() previously to see if simple things like ‘true’ and ‘false’ matched properly. assertEquals()
is different. It is what is often called a ‘deep equals’ or a recursive equals. This looks for comparison in every nested object or array comparing keys and values to determine if two things are exactly the same. assertEquals()
requires more processing, but is useful in comparing objects and arrays.
Now to be fully complete I test the other two bail conditions setting the mocks in user_can_add()
and adding appropriate errors for e_needs_processing()
. For this post’s purposes there is nothing new to be learned by examining each of these, you can look at them in the complete code listing of the tests file at the bottom of this article.
Okay, now let’s have a look at the path when all the guards functions pass. Our approach is very much like the one when the guards fail, but this time our plugin on_load()
function won’t return early (well, it shouldn’t) and the WP_Error
object will be processed (or it should anyway). So, what changes is the number and type of our assertions. Let’s have a look.
/**
* Testing if function properly processes object
*/
public function test_on_load_processing() {
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote-users' ),
'return' => true,
)
);
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
$truucde_error->add( $this->Target_code, $this->Another_msg, $this->Another_data );
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$truucde_error->add( $this->Target_code, $this->White_msg, $this->Target_data );
$orig_result = [];
$orig_result['errors'] = $truucde_error;
$result = on_loaded( $orig_result );
$error_return = $result['errors'];
// Object is WP_Error
$this->assertInstanceOf( WP_Error::class, $error_return);
// Returns correct error codes
$return_codes = $error_return->get_error_codes();
$this->assertContains( $this->Another_code, $return_codes);
$this->assertContains( $this->Target_code, $return_codes);
// Returns correct messages
$return_a_msg = $error_return->get_error_messages( $this->Another_code );
$return_t_msg = $error_return->get_error_messages( $this->Target_code );
$this->assertContains( $this->Another_msg, $return_a_msg);
$this->assertContains( $this->Another_msg, $return_t_msg);
$this->assertNotContains( $this->Black_msg, $return_t_msg);
$this->assertNotContains( $this->White_msg, $return_t_msg);
// Returns data
$this->assertContains( $this->Another_data, $error_return->get_error_data( $this->Another_code));
$this->assertNotContains( $this->Target_data, $error_return->get_error_data( $this->Another_code));
$this->assertContains( $this->Target_data, $error_return->get_error_data( $this->Target_code ));
}
The primary concern here is that and how the WP_Error object is processed. This test starts in a similar way to the previous test: mock is_user_logged_in
and current_user_can
, and create a new WP_Error object
(lines 6 – 23).
Test setup continues with the addition of 4 error messages to the WP_Error object:
- an extra message within a different error code than our target (line 25)
- an extra message within the target error code (line 26)
- a black list message (line 27)
- a white list message (line 28).
Again we simulate a WordPress results array in $orig_results
and insert the prepared error object into the ['errors']
key (lines 30-31).
Then we pass this results array into our on_loaded
function and store it in the $result
variable (line 33). Finally we pull the processed wp_error
object back out and store it in $error_return
for comparison purposes (line 35).
Now we run some assertions. On line 37 we test if the error object returned from our function is an instance of the WP_Error
object.
Then we test (lines 40 to 42) that the returned error object contains the expected error codes. Although our processing should have removed the Black_msg
and White_msg
error messages, both of the codes stored in Target_code
and Another_code
should still be in the object from lines 25 and 26. Note that on line 40 we use WP_Error
object method get_error_codes()
to store an array of error codes in $return_codes
. In lines 41 and 42 we check to see if this array contains the codes we expect.
We do a similar thing in lines 45 – 52 but this time with error messages. In 45 and 46 we store message arrays for each of the error codes using WP_Error
method get_error_messages()
. Then we check that the expected remaining messages are as we expect (48/49) and that the target messages have been removed (51/52).
Finally in lines 55 – 57 we check that error data (often not actually used) associated with our error codes is in its expected form.
And, everything passes. Hurray.
Wrap up
Okay, that wraps up this look at unit testing. We looked at setting the context for a test including mocking outside code, using different assertion types to test expectations against results, and looked at some tips and pitfalls. In the next post we’ll set up for integration testing and testing code within a WordPress environment. Oh, and you can find the completed code for this article below.
<?php /** @noinspection DuplicatedCode */
/**
* TruUcde
*
* @package Truucde
*/
namespace TruUcdeBlog;
require_once 'class-wp-error.php';
use \WP_Error; // need to create empty error objects.
/**
* TruUcde test case
*/
class TruUcdeTest extends \WP_Mock\Tools\TestCase {
/*
* Define all the things
*/
public $Target_code = 'user_email';
public $Black_msg = 'You cannot use that email address to signup. We are having problems with them blocking some of our email. Please use another email provider.';
public $White_msg = 'Sorry, that email address is not allowed!';
public $Target_data = 'User email things.';
public $Another_code = 'user_foolishness';
public $Another_msg = 'The eagle lands at midnight';
public $Another_data = 'Spy games';
/**
* Test setup
*/
public function setUp(): void {
\WP_Mock::setUp();
}
/**
* Test teardown
*/
public function tearDown(): void {
\WP_Mock::tearDown();
}
/**
* Test that hooks are being initialized.
*/
public function test_it_adds_hooks(): void {
// ensure the filter is added.
\WP_Mock::expectFilterAdded(
'wpmu_validate_user_signup',
'TruUcdeBlog\on_loaded'
);
// Now test the init hook method of the class to check if the filter is added.
truucde_init();
$this->assertConditionsMet();
}
/**
* Tests that user checks are working as they should
* TT
*/
public function test_user_can_add_TT() {
// Mock 'is_user_logged' in and 'current_user_can'.
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => true,
)
);
// Run it through the function.
$result = user_can_add();
// And assert
$this->assertSame( true, $result );
}
/**
* Tests that user checks are working as they should
* TF
*/
public function test_user_can_add_TF() {
// Mock 'is_user_logged' in and 'current_user_can'.
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => false,
)
);
// Run it through the function.
$result = user_can_add();
// And assert.
$this->assertSame( false, $result );
}
/**
* Tests that user checks are working as they should
* FT
*/
public function test_user_can_add_FT() {
// Mock 'is_user_logged' in and 'current_user_can'.
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => false,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 0,
'args' => array( 'promote_users' ),
'return' => true,
)
);
// Run it through the function.
$result = user_can_add();
// And assert
$this->assertSame( false, $result );
}
/**
* Tests that user checks are working as they should
* FF
*/
public function test_user_can_add_FF() {
// Mock 'is_user_logged' in and 'current_user_can'.
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => false,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 0,
'args' => array( 'promote_users' ),
'return' => false,
)
);
// Run it through the function.
$result = user_can_add();
// And assert
$this->assertSame( false, $result );
}
/**
* Tests that the error object is valid, non-empty and
* has white/black list errors that need processing
*
* Tests error object empty (4 cases)
*/
public function test_e_needs_processing_empty() {
// Is error object -> is empty (4 cases).
$empty_wp_error = new WP_Error();
$result = e_needs_processing( $empty_wp_error );
$this->assertFalse( $result );
}
/**
* Tests that the error object is valid, non-empty and
* has white/black list errors that need processing
*
* Tests truth table of list msg presence.
*/
public function test_e_needs_processing_lists() {
// TODO: Separate functions
// Doing this the lazy way, adding and removing as I go
// to create the various truth table conditions
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
// FF
$result = e_needs_processing( $truucde_error );
$this->assertFalse( $result );
// TF
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$result = e_needs_processing( $truucde_error );
$this->assertTrue( $result );
// FT
$truucde_error->remove( $this->Target_code );
$truucde_error->add( $this->Target_code, $this->White_msg, $this->Target_data );
$result = e_needs_processing( $truucde_error );
$this->assertTrue( $result );
// TT
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$result = e_needs_processing( $truucde_error );
$this->assertTrue( $result );
}
/**
* Testing if on load conditions failure returns
* original object
*/
// user_can_add() return true, e_needs_processing return false
public function test_on_load_conditions_bail_utef() {
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => true,
)
);
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
$orig_result = array();
$orig_result['errors'] = $truucde_error;
$result = on_loaded( $orig_result );
$this->assertEquals( $orig_result, $result );
}
/**
* Testing if on load conditions failure returns
* original object
*/
// user_can_add() return false, e_needs_processing return true
public function test_on_load_conditions_bail_ufet() {
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => false,
)
);
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$truucde_error->add( $this->Target_code, $this->White_msg, $this->Target_data );
$orig_result = array();
$orig_result['errors'] = $truucde_error;
$result = on_loaded( $orig_result );
$this->assertEquals( $orig_result, $result );
}
/**
* Testing if on load conditions failure returns
* original object
*/
// user_can_add() return true, e_needs_processing return false
public function test_on_load_conditions_bail_ufef() {
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => false,
)
);
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
$orig_result = array();
$orig_result['errors'] = $truucde_error;
$result = on_loaded( $orig_result );
$this->assertEquals( $orig_result, $result );
}
/**
* Testing if function properly processes object
*/
public function test_on_load_processing() {
\WP_Mock::userFunction(
'is_user_logged_in',
array(
'times' => 1,
'return' => true,
)
);
\WP_Mock::userFunction(
'current_user_can',
array(
'times' => 1,
'args' => array( 'promote_users' ),
'return' => true,
)
);
$truucde_error = new WP_Error();
$truucde_error->add( $this->Another_code, $this->Another_msg, $this->Another_data );
$truucde_error->add( $this->Target_code, $this->Another_msg, $this->Another_data );
$truucde_error->add( $this->Target_code, $this->Black_msg, $this->Target_data );
$truucde_error->add( $this->Target_code, $this->White_msg, $this->Target_data );
$orig_result = array();
$orig_result['errors'] = $truucde_error;
$result = on_loaded( $orig_result );
$error_return = $result['errors'];
// Object is WP_Error
$this->assertInstanceOf( WP_Error::class, $error_return );
// Returns correct error codes
$return_codes = $error_return->get_error_codes();
$this->assertContains( $this->Another_code, $return_codes );
$this->assertContains( $this->Target_code, $return_codes );
// Returns correct messages
$return_a_msg = $error_return->get_error_messages( $this->Another_code );
$return_t_msg = $error_return->get_error_messages( $this->Target_code );
$this->assertContains( $this->Another_msg, $return_a_msg );
$this->assertContains( $this->Another_msg, $return_t_msg );
$this->assertNotContains( $this->Black_msg, $return_t_msg );
$this->assertNotContains( $this->White_msg, $return_t_msg );
// Returns data
$this->assertContains( $this->Another_data, $error_return->get_error_data( $this->Another_code ) );
$this->assertNotContains( $this->Target_data, $error_return->get_error_data( $this->Another_code ) );
$this->assertContains( $this->Target_data, $error_return->get_error_data( $this->Target_code ) );
}
}