Better Unit Testing with DataProviders
Ryan Welcher
Posted on November 11, 2020
When unit testing your code, there are times when you need to test the same method or function with a variety of different parameters. For example, we have a function to check to see if a passed email is both valid and not already in the list of known emails:
/**
* Confirm that a new email is both valid and not already in a list of known email addresses.
*
* @param string $new_email The new email we're going to add.
* @param array $list_of_saved_emails The list of currently saved emails to check against.
*
* @return bool
*/
function is_new_and_valid_email( $new_email, $list_of_saved_emails ) {
if ( ! filter_var( $new_email, FILTER_VALIDATE_EMAIL ) ) {
return false;
}
if ( in_array( $new_email, $list_of_saved_emails, true ) ) {
return false;
}
return true;
}
We can run a simple test on this by creating a new test class:
/**
* Class Simple_Tests
*/
class Simple_Tests extends PHPUnit_Framework_TestCase {
/**
* Simple unit test with hardcoded values
*/
function test_is_new_and_valid_email_new_email() {
$expected_result = true;
$actual_result = is_new_and_valid_email( 'new@email.com', array( 'alternate@email.com' ) );
$this->assertSame( $expected_result, $actual_result );
}
}
The test above will pass but it doesn’t cover all of the possibilities that this function could run into in the real world. What happens if the email already exists or is not a real email? We need those tests too.
We have a couple of options to create those tests:
- Write a new test function for each possible combination.
- Use a DataProvider and run the same test using that data it provides.
Option 1: Write more tests.
Let’s start by looking at the first option. We will need to write a new assertion for each of the possible cases. Which looks like this:
/**
* Simple unit test with hardcoded values
*/
function test_is_new_and_valid_email_new_email() {
$expected_result = true;
$actual_result = is_new_and_valid_email( 'new@email.com', array( 'alternate@email.com' ) );
$this->assertSame( $expected_result, $actual_result );
}
function test_is_new_and_valied_email_already_known() {
$expected_result = false;
$actual_result = is_new_and_valid_email( 'new@email.com', array(
'alternate@email.com',
'alternate1@email.com',
'new@email.com',
) );
$this->assertSame( $expected_result, $actual_result );
}
function test_is_new_and_valied_email_invalid_email() {
$expected_result = false;
$actual_result = is_new_and_valid_email( 'not-an-email', array() );
$this->assertSame( $expected_result, $actual_result );
}
These tests are valid but there is a fair amount of duplication in the code. The only real differences between the three assertions are the expected result, the email we’re adding and the known list. The rest of the code is the same.
What if we have a way of passing those three items as parameters to the same test? Well we do, it’s called a DataProvider.
Option 2 : Use a DataProvider
At its base, a DataProvider is a function that returns an array consisting of the parameters for each assertion to be run:
/**
* Example DataProvider
*/
function example_dataprovider() {
return array(
'param_for_test_1',
'param_for_test_2',
'param_for_test_3',
);
}
In practice, DataProviders usually returns an array of values for each test simply because at the very least you will want to pass the expected result of the test and a value to pass to the code your testing.
We’ll need to refactor our test code a bit in order to use a DataProvider:
function test_is_new_and_valid_email( $expected_result, $new_email, $list_of_known_emails ) {
$actual_result = is_new_and_valid_email( $new_email, $list_of_saved_emails );
$this->assertSame( $expected_result, $actual_result );
}
As you can see, the testing function now accepts 3 parameters that will be passed to it from the DataProvider which looks like this:
function data_is_new_and_valid_email() {
return array(
// Valid email address that doesn't exist.
array(
true,
'new@email.com',
array(
'alternate@email.com',
'alternate2@email.com',
),
),
// Valid email that already exists.
array(
false,
'new@email.com',
array(
'alternate@email.com',
'alternate1@email.com',
'new@email.com',
),
),
// Invalid email.
array(
false,
'not-an-email',
array(),
),
);
}
** Just an aside here, the name you use for the Data Provider doesn’t matter, I tend to use the convention of using the test function name and changing test_ to data_ to make the association clear. ***
Each of the arrays being returned contains the parameters, in order, that the test function is expecting; The $expected_result , the $new_email we’re adding and the $list_of_known_emails
At this point, we have our testing function and the DataProvider ready but they don’t know about each other. Connecting them is easy, we simply a new annotation to the testing function and PHPUnit will make the connection for us:
/**
* Simple unit test with a dataprovider so we can run many assertions in one test.
*
* The dataProvider annotation tells PHPUnit which function it should use
* for the data to test.
*
* @dataProvider data_is_new_and_valid_email
*/
function test_is_new_and_valid_email( $expected_result, $new_email, $list_of_saved_emails ) {
$actual_result = is_new_and_valid_email( $new_email, $list_of_saved_emails );
$this->assertSame( $expected_result, $actual_result );
}
Now when we run our tests, the test_is_new_and_valid_email test will run for each index in the DataProvider array and will be passed their parameters. If you ever need to add another test case, it’s a simple as adding a new array to the DataProvider!
As you can see, even though there is a little more setup involved for a DataProvider, your tests become more concise and easier to maintain and extend.
Here is the final test class with some comments:
/**
* Class Simple_Tests
*
* Tests that just use PHPUnit
*
* @group simple
*/
class Simple_Tests extends PHPUnit_Framework_TestCase {
/**
* Simple unit test with a dataprovider so we can run many assertions in one test.
*
* The dataProvider annotation tells PHPUnit which function it should use
* for the data to test.
*
*
* @param bool $expected_result
* @param string $new_email
* @param array|mixed $list_of_saved_emails
*
* @dataProvider data_is_new_and_valid_email
*/
function test_is_new_and_valid_email( $expected_result, $new_email, $list_of_saved_emails ) {
$actual_result = is_new_and_valid_email( $new_email, $list_of_saved_emails );
$this->assertSame( $expected_result, $actual_result );
}
/**
* Data provider.
*
* @return array {
* @type array {
* @type bool $expected_result
* @type string $new_email
* @type array $list_of_saved_emails
* }
* }
*/
function data_is_new_and_valid_email() {
return array(
// Valid email address that doesn't exist.
array(
true,
'new@email.com',
array(
'alternate@email.com',
'alternate2@email.com',
),
),
// Valid email that already exists.
array(
false,
'new@email.com',
array(
'alternate@email.com',
'alternate1@email.com',
'new@email.com',
),
),
// Invalid email.
array(
false,
'not-an-email',
array(),
),
);
}
}
What do you think of dataProviders? Are they useful? Please let me know in the comments!
The post Better Unit Testing with DataProviders appeared first on Ryan Welcher.
Posted on November 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.