Maintainable Tests With Data Providers & Callable in Laravel

Maintainable Tests With Data Providers & Callable in Laravel

ยท

5 min read

Testing is an essential component of the software development process that helps to maintain product quality and reliability. It involves the verification and validation of software products to ensure they meet the intended requirements and function as expected.

The key features of testing include early detection of bugs and errors, which helps to save time and resources by identifying and resolving issues early on in the development cycle. Testing also helps to ensure that end-user expectations are met, which is critical for achieving customer satisfaction. In addition, testing can lead to the improvement of code quality and maintain code integrity without system failure. Finally, testing enables system delivery with confidence, as it ensures that the software is thoroughly tested and ready for deployment.

Testing in Laravel

Laravel offers a built-in framework for testing that simplifies the process of writing and executing tests for an application. Among its features is data-driven testing, which involves passing a set of test data as parameters to the test method using a data provider method.


Testing with Data providers

In Laravel, we can pass sets of test data with data providers, minimizing the need for tests on different cases. Generally, we use data providers to pass the test cases and the expected value to be asserted.

<?php
namespace Tests\Unit;
use PhpUnit\Framework\TestsCase;
class LarvelTest extends TestCase
{    
    public function oddEvenTestDataProvider(){
        return [
            [1,'odd'],
            [2,'even'],
            [-11,'odd'],
            [-10,'even'],
        ];
    }

    /**
    * @param int $value
    * @param string $expected
    * 
    * @dataprovider oddEvenTestDataProvider
    */
    public function testOddEven($value,$expected){
        $integerType= $value % 2 == 0?'even':'odd';
        $this->assertEquals($expected,$integerType);
    }
}

In this instance, we provided test cases to verify both even and odd integers, as well as a few positive and negative integers, along with their expected outcomes. The oddEvenTestDataProvider test then checks whether the actual results match the expected values.

The main test function may become cluttered with a large number of manipulations needed to perform assertions based on test data. However, this issue can be resolved by using data providers to define assertions within callables, which ultimately results in cleaner and more organized testing code.


Data providers with callables

With the regular test, we can set test data and expectations to assert. But sometimes, we might need to conduct assertions dynamically based on a condition. There might come situations, where code with multiple conditions just to decide what to be asserted for data. This will make test complex to keep track of what is being tested. To manage this possible complexity, callables can be used with data providers. With callable we can establish an intended assertion per test data. This process will help the test focus on assertion, meanwhile, data providers decide what assertion to carry out.

Data providers are executed before the Laravel service container is fully operational, which means that certain framework-related features may not be available when defining test data. Nevertheless, by using callables, it is possible to ensure that the required framework features are made available for execution during testing.

Here is an example of a test using data providers with callables.

<?php

namespace Tests\Unit\Http\Services\Common;

use Generator;
use Tests\TestCase;

class OddEvenTest extends TestCase
{

    public function integerProvider(): Generator
    {        
            #test-data-1
            yield [
                1,
                function ($value) {
                    $this->assertEquals('odd', 
                                $this->getIntegerType($value));
                    $this->assertEquals('positive', 
                                $this->getIntegerIndex($value));
                }
            ];

            #test-data-2
            yield [
                0,
                function ($value) {
                    $this->assertEquals('even', 
                                $this->getIntegerType($value));
                }
            ];

            #test-data-3
            yield [
                -2,
                function ($value) {
                    $this->assertEquals('even', 
                            $this->getIntegerType($value));
                    $this->assertEquals('negative', 
                    $this->getIntegerIndex($value));
                }
            ];

    }

    /**
     * @dataProvider integerProvider
     */
    public function testIntegerIndexAndType(
                            int $value, 
                            callable $assert)    
    {
        $assert($value);
    }

    private function getIntegerIndex($value): string
    {
        if($value > 0)return 'positive';
        if($value < 0)return 'negative';
        return '';
    }

    private function getIntegerType($value): string
    {
        return $value % 2 == 0 ? 'even' : 'odd';
    }
}

As demonstrated in the example above, the test verifies both the type and index of the integer test data. Each set of data supplied via the integerProvider includes an integer value and a callable that contains a unique assertion that distinguishes it from other test cases.

Here,

#test-data-1: The data provide 1 as a value along with the callable function that asserts it is a positive and odd number.

#test-data-2: The data provide 0 as a value along with the callable function that asserts it is an even number. Here assertion for integer index is not done as the getIntegerIndex function returns an empty string for value 0 which could be ignored, thus showcasing the benefit of callable usage with the test.

#test-data-3: The data provide 2 as a value along with the callable that asserts it is a negative and even number. Here also the assertions defined completely differ from other test data.


Conclusion

To conclude, there are ways to write maintainable tests ๐Ÿ‘จโ€๐Ÿ’ป, and one effective method is by using data providers with callables. As demonstrated above, this practice isolates the main test function and delegates the responsibility of determining assertions to the data providers.

Although there may be tradeoffs to using this approach, they can be mitigated by careful implementation and can ultimately add value to the test suite. While the example may not perfectly reflect a real-world scenario, it provides a good starting point to develop a better test suite. ๐Ÿš€

If you have any additional ideas, suggestions, or feedback regarding the topic discussed or any other related matter, please do not hesitate to share them with us. So, feel free to share your thoughts!

Thank you and Keep on learning.๐Ÿš€๐Ÿš€

ย