Build, Test and Document REST APIs in PHP & Laravel — Part II — Testing

Regina Sharon G
7 min readNov 5, 2021

--

Co-Authored by Santhosh Krishnamoorthy, Jotheeswaran Gnanasekaran, Harsha Sudheer, Charan Sai & Swati Akkishetti.

Welcome to Part-II of the series on Building, Testing and Documenting REST APIs in PHP+Laravel. If you haven’t gone through the previous part, I highly recommend that you do so. You can find it here.

In this instalment we will be focusing on writing test cases for the REST APIs that we built in Part-I of the series.

Unit testing and its benefits

Unit testing is all about testing small, individual units of code to determine their conformity to the designed specifications which would include testing associated data and usage. A unit here is a small piece of code or any single function.

Benefits:

  • Make it easier for us to refactor code and significantly improve the quality of code.
  • Makes sure that we don’t break any existing functionality that has previously been thoroughly tested.
  • Faster debugging as it is easier to isolate the cause of a bug by identifying where a test fails.
  • The tests act as documentation to an app and if you are new to part of a project’s code you should be able to look at the relevant tests to see the expected outcomes of it’s functionality.

Testing in PHP

PHPUnit, created by Sebastian Bergmann is one of the most popular testing frameworks for PHP. We will be using that to write our test cases here.

Installing PHPUnit

Laravel comes with PHPUnit pre-configured. A Laravel project contains a test folder with two subfolders namely Feature and Unit, containing sample test cases. Few helper methods are also provided to ease your test case development workflow.

In case PHPUnit isn’t present, you can verify this by running this command:

vendor/bin/phpunit — version

If it says ‘no such file or directory’, then go ahead and install PHPUnit manually using the command below :

composer require — dev phpunit/phpunit

Writing Tests

A new test class can be created using Laravel’s artisan command :

php artisan make:test <Test Case Name>

The created test code looks something like this :

  • The created test class extends from the ‘PHPUnit\Framework\TestCase’ class.
  • The names of the actual test methods usually follow a convention as follows: test<”brief description of what the test case should do”>, For eg: testShouldValidateUserDetails()
  • OR you can annotate a method using @test in the block comment above the function definition to turn it into a test case method
  • The setUp and tearDown methods are executed before and after each test respectively. These methods are useful to initialise and release any resources that might be needed for the test.

Assertions

At the most basic level, the written code is tested by comparing its behaviour against what is expected by using the PHPUnit’s assertion methods.

A few of them are shown below:

  • assertEquals($expected, $actual) — Test case Passes only if the $expected and $actual values are equal, else it is marked as Failed
  • assertTrue($result) — Test case Passes only if the $result value is true else it is marked as Failed.
  • assertFalse($result) — Same as above but with reversed expectation.

More information regarding these Assertion methods can be found here — PHPUnitAssertions

Mocking

Mocking is a powerful technique in Unit testing. It is used to simulate the behaviour of an object or a class. In unit testing, you usually test the methods of a class in isolation. However, the class you intend to test could be dependent on other classes. In this situation, you mock those dependencies and their methods using a mocking framework and use those mocked methods to test the functionality of the class in isolation.

With that quick introduction to testing in PHP out of the way, let us get down to writing test cases for the code that we wrote in Part-I of this series.

In our case, we will be mocking the Repository layer which in-turn mimics a response from a database. It’s useful when we do not want to call a database, but rather test a specific piece of functionality that would interact with an expected response from a database.

First, let us start by creating a new test class. By default, tests will be added in the path tests/Feature folder.

Artisan command to create test class under Unit folder :

php artisan make:test BookServiceTest — unit

This is the skeleton that gets created :

Let us now populate it with our setup and test case methods.

We create the setUp method to initialise the objects that we need to work with :

  • Mock objects of the BookRepository and AuthorRepository classes.
  • An instance of the BookService class by passing in the mocked repository objects.
class BookServiceTest extends TestCase
{
private $bookRepositoryMock;
private $authorRepositoryMock;
private $bookService;
protected function setUp(): void
{
parent::setUp();
$this->bookRepositoryMock =
$this->createMock(IBookRepository::class);
$this->authorRepositoryMock =
$this->createMock(IAuthorRepository::class);
$this->bookService =
new BookService($this->bookRepositoryMock,
$this->authorRepositoryMock);
}

Next step is to add a test method.

Create a method shouldAddBookWhenAuthorAvailable. This is going to test the addition of a new Book.

  • We are creating an object of StdClass called $authorDetails and assigning its id with a numeric value.
$authorDetails = new stdClass();
$authorDetails->id = "13";
  • Now we need to mock the method getAuthorIdFromAuthorName in the AuthorRepository class to return the expected value(Object we created — $authorDetails).
$this->authorRepositoryMock->method('getAuthorIdFromAuthorName')
->willReturn($authorDetails);
  • Similarly, we need to mock the method addBook in the BookRepository class to return true(which means that the book is successfully added).
$this->bookRepositoryMock->method('addBook')->willReturn(true);
  • Call the method that we need to test by passing the required arguments(title, price and author) and it will return the actual message.
$actualMessage = $this->bookService->addBook('Never wants to die', 300, ['first_name'=>'Nicholas']);
  • Finally, assert the expected message(‘Book is successfully added) and actual message($actualMessage) using the assertEquals method.
assertEquals("Book is added successfully", $actualMessage);/**
* @test
*/
public function shouldAddBookWhenAuthorAvailable()
{
$authorDetails = new stdClass();
$authorDetails->id = "13";
$this->authorRepositoryMock->method('getAuthorIdFromAuthorName')
->willReturn($authorDetails);
$this->bookRepositoryMock->method('addBook')->willReturn(true); $actualMessage = $this->bookService->addBook('Never wants to die',
300, ['first_name'=>'Nicholas']);
assertEquals("Book is added successfully", $actualMessage);
}

Let us add another test case named shouldNotAddBookWhenAuthorNotAvailable.

As the method name describes, this will verify that the addBook method indeed doesn’t add a Book when the Author doesn’t exist.

/**
* @test
*/
public function shouldNotAddBookWhenAuthorNotAvailable()
{
$authorDetails = new stdClass();
$authorDetails->id = "13";
$this->authorRepositoryMock->method('getAuthorIdFromAuthorName')
->willReturn($authorDetails);
$this->bookRepositoryMock->method('addBook')->willReturn(false); $actualMessage = $this->bookService->addBook('Never wants to die',
300, ['first_name'=>'Nicholas','last_name'=>'Sparks',
'email'=>'nicholassparks@gmail.com'] );
assertEquals("We are not able to add a book", $actualMessage);
}

Following the same process as above, you can add more test methods to ensure that you have covered most of the scenarios.

With the test cases created, let us run them.

Executing Test Cases

Assuming that PHPUnit is installed , you can use the command below to run all test cases.

./vendor/bin/phpunit

When running the test cases for the first time you might encounter a ‘Key Not Found Error’. To fix that, execute the commands below, to generate the key.

php artisan key:generatephp artisan config:cache

You can pass a specific test file that you want to run by using a command like below :

./vendor/bin/phpunit tests/Unit/BookServiceTest.php

OR you can go more granular and specify a particular test function to be executed using the ‘ — filter’ option like below :

./vendor/bin/phpunit — filter shouldAddBookWhenAuthorAvailable

When you run your tests it will output how many assertions have been tested and succeeded. For a successful pass you should see something like this:-

Assessing the Test Coverage

Test coverage is the process you use to determine whether all the code paths are covered by your test cases. To assess whether you are testing everything that you’re supposed to test.

By getting data about your test coverage, you can understand the areas that are still being missed by the tests. This will help you better the quality of your test suite.

To get data on the test coverage you need to install a utility called ‘xdebug’

Follow these commands for MacOS :

brew install phppecl install xdebugbrew doctorecho ‘export PATH=”/usr/local/sbin:$PATH”’ >> ~/.zshrcsource ~/.zshrc

brew doctor: brew doctor command is used to get the xdebug path

echo ‘export PATH=”/usr/local/sbin:$PATH”’ >> ~/.zshrc: This command copies the path to the .zshrc file.

source ~/.zshrc: It loads the file and refreshes your current shell’s environment.

For installation on Windows and Linux please refer to the documentation here — https://xdebug.org/docs/install

With ‘xdebug’ installed, you can run the test cases by choosing the ‘with Coverage’ option in an IDE like IntelliJ as below :

The Coverage report is displayed as you see below

Congratulations! Now you have written unit test cases and verified that the API methods are indeed working as expected.

--

--