8

Using TDD with SOLID Principles

When I first started programming, I instantly got addicted to it. I felt so excited about the thought of coming up with a solution to a problem using programs and my own imagination. Back in school, there was a time when the instructor gave us the task of solving some simple algebraic challenges using Turbo-C. I had goosebumps and felt very excited as I quickly realized I could just write programs to solve these types of challenges repeatedly. Write the program once, pass different arguments, and get different results. I loved it. I remember a challenge to compute the height of a bridge if someone is standing on it, drops a ball, and hears a sound after several seconds. Easy! Now, I can just use my program to compute the height of the bridge for me repeatedly. Now, I don’t have to keep remembering that the Earth’s gravitational acceleration is at around 9.8 m/s2 – I can just declare it in the program! I learned that in programming, I can follow my own rules to get from point A to point B. Give me a task, and I can come up with a solution using my own imagination to finish the task. This, for me, is the best thing about programming. I was one proud spaghetti-code-writing machine. I didn’t care about how clean my code was – I just needed to solve problems using code! Learning about other programming languages made me even more excited, and I thought the possibilities were endless – if the task or challenge did not defy the laws of physics, I thought it could be solved using programming! I did not pay attention to code cleanliness or maintainability. What are those? I don’t need those!

When I started working professionally as a software developer, I continued with my mindset of just enjoying solving problems using programming. I didn’t care how disorganized my solutions were – they solved the problems, and my employers and clients were happy. Done, I’m out of here. Too easy. I thought I knew everything and that I was unstoppable. Oh boy, I was so wrong. The more I learned, the more I realized how little I knew how to program.

As I continued working on more complex projects with other developers while having to maintain these projects, I learned the hard way how difficult I had made my life by writing code I couldn’t easily maintain myself. I’m probably not the only developer on the planet to have experienced this problem. I was sure other people had encountered these issues before, and I was sure there were solutions out there. One of the solutions that helped make my life a lot easier was by trying to follow the SOLID principles by Robert C. Martin. They really helped change my programming life, and using these principles with Test-Driven Development (TDD) made my programming life even easier! There are more principles and architectural design patterns out there to help make your application more maintainable, but in this chapter, we will be focusing on the SOLID principles one by one while doing TDD.

We’ll go through the process of interpreting a Jira ticket into a BDD test, which, in turn, will help us in creating our integration tests, down to the development of the solution code. Then, one by one, we will go through each of the SOLID principles by using TDD as you would do in a real project.

In this chapter, we will go through the following topics:

  • Jira to BDD to TDD
  • TDD with the Single-Responsibility Principle
  • TDD with the Open-Closed Principle
  • TDD with the Liskov Substitution Principle
  • TDD with the Interface Segregation Principle
  • TDD with the Dependency Inversion Principle

Technical requirements

In this chapter, the reader needs to use the base code from the repository found at https://github.com/PacktPublishing/Test-Driven-Development-with-PHP-8/tree/main/Chapter%208.

Preparing the development environment for the chapter

First, get the base code for this chapter found at https://github.com/PacktPublishing/Test-Driven-Development-with-PHP-8/tree/main/Chapter%206/base/phptdd or simply run the following command:

curl -Lo phptdd.zip "https://github.com/PacktPublishing/Test-Driven-Development-with-PHP-8/raw/main/Chapter%208/base.zip" && unzip -o phptdd.zip && cd base && ./demoSetup.sh

To run the containers and execute the commands in this chapter, you should be inside the docker-server-web-1 container.

Run the following command to confirm the container name for our web server:

docker ps

To run the containers, run the following command from the /docker directory from the repository in your host machine:

docker-compose build && docker-compose up -d
docker exec -it docker-server-web-1 /bin/bash

Once inside the container, run the following commands to install the libraries required through composer:

/var/www/html/symfony# ./setup.sh
/var/www/html/behat# ./setup.sh 

Jira to BDD to TDD

The SOLID principles, as defined by Robert C. Martin, are a set of coding guidelines or standards that help developers write more organized, decoupled, maintainable, extensible software. In this chapter, we’ll go through them one by one, but we will try to simulate the process by working on a real project and then implementing each of the principles.

In this chapter, we will be writing solution code that will try to adhere to the SOLID principles, but before that, we need an example problem to solve. As we did in Chapter 7, Building Solution Code with BDD and TDD, we’ll start with a Jira ticket, write some Gherkin features, write Behat tests, write integration and unit tests, and then write the SOLID-adhering solution code as depicted in the following flowchart:

Figure 8.1 – Development flow

Figure 8.1 – Development flow

Let’s use one of the Jira tickets we created in Chapter 2, Understanding and Organizing the Business Requirements for Our Project. We created a story to let a logged-in user input and save some toy car model data. This will be a nice simple feature to use to demonstrate the SOLID principles:

Figure 8.2 – Ticket for creating toy model data

Figure 8.2 – Ticket for creating toy model data

As we did in Chapter 7, Building Solution Code with BDD and TDD, create a new git branch for your Jira ticket. Check out the git branch from the repository you set up in Chapter 2, Understanding and Organizing the Business Requirements for Our Project, and let’s start writing some tests and programs!

Before we start learning about the SOLID principles, first, we need to work on the BDD tests that will drive us to write the solution code while trying to follow the SOLID principles. Remember, we always need to start with failing tests. Next, to start with BDD, we need to write a Gherkin feature first.

Gherkin feature

Let’s start by writing a Gherkin feature to describe what behavior we expect to build. Create the following feature file with the following content inside the behat directory:

codebase/behat/features/create_toy_car_record.feature

Feature: Clerk creates new toy car record
  In order to have a collection of toy car model records
  As an Inventory Clerk
  I need to be able to create a single record
  Scenario: Create new record
    Given I am in the inventory system page
    When I submit the form with correct details
    Then I should see a success message

Now that we have our feature, let’s generate the Behat PHP context class for it.

Behat context

We will now take the Gherkin feature and make a PHP context class for it. Follow these steps:

  1. First, update the behat.yml file:

codebase/behat/behat.yml

default:
  suites:
    default:
      contexts:
        - FeatureContext
        - HomeContext
    suite_a:
      contexts:
        - InventoryClerkRegistrationContext
    suite_create:
      contexts:
        - CreateToyCarRecordContext
  1. After updating the main behat.yml file, run the following commands to create the PHP context class:
    /var/www/html/behat# ./vendor/bin/behat --init
    /var/www/html/behat# ./vendor/bin/behat features/
        create_toy_car_record.feature --append-snippets –
            suite=suite_create
  2. There should now be a new class created in features/bootstrap/CreateToyCarRecordContext.php. Refactor the iAmInTheInventorySystemPage method so that it throws Exception.
  3. Next, let’s make sure we can execute this feature test by running the following command:
    /var/www/html/behat# ./vendor/bin/behat features/
    create_toy_car_record.feature --suite=suite_create

You should then see the following test result:

Figure 8.3 – Failed test

Figure 8.3 – Failed test

Good – now, we know that the Behat test for this feature can be executed and fails as expected, so let’s move on to the Symfony application.

Functional test

The Behat test we created is already a functional test – do we still have to create a functional test inside the Symfony directory? I think this is optional, but it will help us to quickly run basic smoke tests – for example, if we want to quickly check whether our controller loads and doesn’t encounter a fatal error. We don’t need to run the bigger and slower Behat test to find that out:

  1. Create the following test class with the following content:

codebase/symfony/tests/Functional/Controller/InventoryAdminControllerTest.php

<?php
namespace AppTestsFunctionalController;
use SymfonyBundleFrameworkBundleTestWebTestCase;
class InventoryControllerTest extends WebTestCase
{
    public function testCanLoadIndex(): void
    {
        $client     = static::createClient();
        $client->request(‘GET’, ‘/inventory-admin’);
        $this->assertResponseIsSuccessful();
    }
}
  1. After creating our controller test class, let’s run the following command to make sure that PHPUnit can execute this test and that it fails:
    /var/www/html/symfony# ./vendor/bin/phpunit --filter 
        InventoryAdminControllerTest

After running the test, make sure that you get a test failure. Remember the red phase?

Great – we can forget about creating the controller for now. Let’s move on to the integration tests. These tests will be used to develop the mechanism to persist the toy car model in the database.

Integration test

We will now need to start writing integration tests that will help us write the code to persist or create a new toy car model. After passing these tests, then we can go back to the Behat tests we created earlier and make sure they pass:

  1. Create the following test class, with the following content:

codebase/symfony/tests/Integration/Processor/ToyCarProcessorTest.php

<?php
namespace AppTestsIntegrationRepository;
use SymfonyBundleFrameworkBundleTest
    KernelTestCase;
class ToyCarProcessorTest extends KernelTestCase
{
    public function testCanCreate()
    {
        $this->fail(“--- RED ---”);
    }
}
  1. After creating the test class, make sure that PHPUnit can recognize the new test class by running the following command:
    /var/www/html/symfony# ./vendor/bin/phpunit --filter ToyCarRepositoryTest
  2. After running the command, you should see the familiar and soothing PHPUnit failure result:
Figure 8.4 – Failing processor test

Figure 8.4 – Failing processor test

Now that we have a failing integration test, let’s build the code to pass it. We want to be able to persist a new toy car model into the persistence layer which is our database. Do we even have a DB table for it? Nope, not yet. But we don’t care. We can continue working on the solution code. Next, we will be trying to follow the Single-Responsibility Principle (SRP) to write our solution code.

TDD with the Single-Responsibility Principle

Let’s start with what I think is one of the most important principles in the SOLID principles. Are you familiar with god classes or objects – where one class can do almost everything? A single class for login, registration, displaying registered users, and so on? If there are two developers working on the same god class, can you already imagine how challenging that can be? And what happens after you deploy it to production and then an issue is found in the part where you display a list of registered users? You will have to change or fix that god class, but now the same class for login and registration has been modified and these processes may be compromised too. You run a bigger risk of introducing regressions to your login and registration functionalities by just trying to fix the list of registered users. You fix one feature, and there’s a greater risk of breaking other features.

This is where the SRP will start to make sense. The SRP mandates that a class should only have one main responsibility, and one reason to be changed. Is it that simple? Sometimes not. A Login class should only know about letting a user log in, and not have the program responsible for displaying a list of registered users or checking out a shopping cart, but sometimes where to draw the line can become very subjective.

Next, we’ll start writing the actual solution code while trying to implement the SRP.

Writing the solution code

We have a failing test that tests whether our application can create a toy car model and persist it in the database, but we don’t even have a database table for it yet. It’s okay – we will only focus on the PHP side of things for now.

Model class

It’s better for our processor PHP class to deal with objects and not to directly know about database table rows and so on. Let’s create a Plain Old PHP Object (POPO) that will represent what a toy car model is, without caring about the database structure:

  1. Create the following file with the following content:

codebase/symfony/src/Model/ToyCar.php

<?php
namespace AppModel;
class ToyCar
{
    /**
     * @var int
     */
    private $id;
    /**
     * @var string
     */
    private $name;
    /**
     * @var CarManufacturer
     */
    private $manufacturer;
    /**
     * @var ToyColor
     */
    private $colour;
    /**
     * @var int
     */
    private $year; 
}

After declaring the properties, it’s best to generate the accessors and mutators for all of these properties, rather than accessing them directly.

As you can see, this is just a POPO class. Nothing fancy. No information whatsoever about how to persist it in our database. Its responsibility is just to be a model that represents what a toy car is.

  1. Let’s also create the CarManufacturer and ToyColor models. Create the following classes with the following content:

codebase/symfony/src/Model/ToyColor.php

<?php
namespace AppModel;
class ToyColor
{
    /**
     * @var int
     */
    private $id;
    /**
     * @var string
     */
    private $name; 
}

After declaring the properties, generate the accessors and mutators for the class.

  1. See the following for the car manufacturer:

codebase/symfony/src/Model/CarManufacturer.php

<?php
namespace AppModel;
class CarManufacturer
{
    /**
     * @var int
     */
    private $id;
    /**
     * @var string
     */
    private $name; 
}

Now, generate the accessors and mutators for this class as well.

Now, we have the main ToyCar model, which is also using the ToyColor and CarManufacturer models. As you can see, as with the ToyCar model, these two classes are not responsible for persisting or reading data either.

As you remember, we are using the Doctrine ORM as a tool to interact with our database. We can also use Doctrine entities directly in our processor class if we want to, but that would mean that our processor class would now be using a class that has a dependency on Doctrine. What if we need to use a different ORM? To keep things a little bit less coupled, we will just use codebase/symfony/src/Model/ToyCar.php in the processor class we will be creating next.

Processor class

For us to create and persist a toy car model, we will need a class that will need to process it for us. The thing is, we still don’t have a database at this stage – where do we persist the toy car model? For now, nowhere, but we can still pass the test:

  1. Create the following interface with the following content:

codebase/symfony/src/DAL/Writer/WriterInterface.php

<?php
namespace AppDALWriter;
interface WriterInterface
{
    /**
     * @param $model
     * @return bool
     */
    public function write($model): bool;
}

We created a very simple interface that our data-writer objects can implement. We’ll then use this interface for our processor class.

  1. Now, let’s create the toy car workflow or processor class. Create the following class with the following content:

codebase/symfony/src/Processor/ToyCarProcessor.php

<?php
namespace AppProcessor;
use AppDALWriterWriterInterface;
use AppModelToyCar;
use AppValidatorToyCarValidationException;
class ToyCarProcessor
{
    /**
     * @var WriterInterface
     */
    private $dataWriter;
    /**
     * @param ToyCar $toyCar
     * @return bool
     * @throws ToyCarValidationException
     */
    public function create(ToyCar $toyCar)
    {
        // Do some validation here
        $this->validate($toyCar);
        // Write the data
        $result = $this->getDataWriter()->
            write($toyCar);
        // Do other stuff.
        return $result;
    }
    /**
     * @param ToyCar $toyCar
     * @throws ToyCarValidationException
     */
    public function validate(ToyCar $toyCar)
    {
        if (is_null($toyCar->getName())) {
            throw new ToyCarValidationException
                (‘Invalid Toy Car Data’);
        }
    }
    /**
     * @return WriterInterface
     */
    public function getDataWriter(): WriterInterface
    {
        return $this->dataWriter;
    }
    /**
     * @param WriterInterface $dataWriter
     */
    public function setDataWriter(WriterInterface 
        $dataWriter): void
    {
        $this->dataWriter = $dataWriter;
    }
}

We have created a processor class that has a create method that accepts the toy car model we created previously, and then tries to write the model using an instance of a writer class that doesn’t exist. What if another developer in your company is working on the data-writer class and it will take him 2 weeks to complete it? Do you wait for 2 weeks to pass your integration test?

If your processor class must validate the data and do other things after the data has been written into the database, should those programs be delayed too just because you are waiting for the other developer to complete their work? Probably not! We can use test doubles to replace the missing dependencies for now.

Test doubles

Most of the time, it’s just difficult or impractical to be able to run a test against a feature with all its dependencies already built. Sometimes, we need to have a solution to be able to test the specific feature we want, even if we have not built the other dependencies yet, or simply want to isolate or only focus our test on a certain feature. Here, we can use test doubles. You can read more about test doubles for PHPUnit at https://phpunit.readthedocs.io/en/9.5/test-doubles.html.

Mock and Stub

The processor class we just created needs a concrete instance of ToyValidatorInterface and WriterInterface. Since we have not created those classes yet, we can still proceed in passing the test just by using a Mock object. In PHPUnit, the Mock object is an interface that extends the Stub interface. This means that in the code, a Mock object is an implementation of a Stub interface. The process of replacing the instances of ToyValidatorInterface and WriterInterface with a Mock object and setting a return value when a specific method is executed is called stubbing. Let’s try it for real:

  1. Go back to the ToyCarProcessorTest class and refactor it with the following content:

codebase/symfony/tests/Integration/Processor/ToyCarProcessorTest.php

<?php
namespace AppTestsIntegrationRepository;
use AppDALWriterWriterInterface;
use AppModelCarManufacturer;
use AppModelToyCar;
use AppModelToyColor;
use AppProcessorToyCarProcessor;
use SymfonyBundleFrameworkBundleTest
    KernelTestCase;
class ToyCarProcessorTest extends KernelTestCase
{
    /**
     * @param ToyCar $toyCarModel
     * @throws AppValidator
         ToyCarValidationException
     * @dataProvider provideToyCarModel
     */
    public function testCanCreate
       (ToyCar $toyCarModel): void
    {
        // Mock: Data writer
        $toyWriterStub = $this->createMock
            (WriterInterface::class);
        $toyWriterStub
            ->method(‘write’)
            ->willReturn(true);
        // Processor Class
        $processor = new ToyCarProcessor();
        $processor->setDataWriter($toyWriterStub);
        // Execute
        $result = $processor->create($toyCarModel);
        $this->assertTrue($result);
    }
    public function provideToyCarModel(): array
    {
        // Toy Car Color
        $toyColor = new ToyColor();
        $toyColor->setName(‘Black’);
        // Car Manufacturer
        $carManufacturer = new CarManufacturer();
        $carManufacturer->setName(‘Ford’);
        // Toy Car
        $toyCarModel = new ToyCar();
        $toyCarModel->setName(‘Mustang’);
        $toyCarModel->setColour($toyColor);
        $toyCarModel->setManufacturer
            ($carManufacturer);
        $toyCarModel->setYear(1968);
        return [
            [$toyCarModel],
        ];
    }
}

In the testCanCreate function here, we are creating mock objects for the ValidationModel, ToyCarValidator, and ToyCarWriter classes. We then instantiate the main ToyCarCreator class while passing the mock ToyCarValidator and ToyCarWriter classes into its constructor. This is called dependency injection, which will be discussed further later in the chapter. Lastly, we then run the ToyCarCreator’s create method to simulate a developer trying to create a new toy car record:

  1. Let’s run the test by entering the following command and see what result we get:
    /var/www/html/symfony# ./vendor/bin/phpunit --filter ToyCarProcessorTest

You should then see the following result:

Figure 8.5 – Passed the test using a stub

Figure 8.5 – Passed the test using a stub

We passed the test, even though we have not really persisted anything in the database yet. It’s very common in bigger and more complex projects that you’ll have to rely on test doubles just to isolate and focus on your test even if other dependencies are either not built yet or are too cumbersome to include as a part of your test.

Now going back to the SRP, our ToyCarProcessor now has two responsibilities – to validate and create a toy car model. Equally, other developers are using your class’s validate method. Let’s refactor our code to redefine the focus and responsibility of our ToyCarProcessor class:

  1. Rename the following classes:
    • ToyCarProcessor.php to ToyCarCreator.php
    • ToyCarProcessorTest.php to ToyCarCreatorTest.php
  2. Next, let’s refactor the ToyCarCreatorTest.php class. Open the following class and replace the content with the following:

codebase/symfony/tests/Integration/Processor/ToyCarCreatorTest.php

<?php
namespace AppTestsIntegrationRepository;
use AppDALWriterWriterInterface;
use AppModelCarManufacturer;
use AppModelToyCar;
use AppModelToyColor;
use AppProcessorToyCarCreator;
use AppValidatorValidatorInterface;
use SymfonyBundleFrameworkBundleTestKernelTestCase;
class ToyCarCreatorTest extends KernelTestCase
{
    /**
     * @param ToyCar $toyCarModel
     * @throws AppValidator
         ToyCarValidationException
     * @dataProvider provideToyCarModel
     */
    public function testCanCreate
        (ToyCar $toyCarModel): void
    {
        // Mock 1: Validator
        $validatorStub = $this->createMock
            (ValidatorInterface::class);
        $validatorStub
            ->method(‘validate’)
            ->willReturn(true);
        // Mock 2: Data writer
        $toyWriterStub = $this->createMock
            (WriterInterface::class);
        $toyWriterStub
            ->method(‘write’)
            ->willReturn(true);
        // Processor Class
        $processor = new ToyCarCreator();
        $processor->setValidator($validatorStub);
        $processor->setDataWriter($toyWriterStub);
        // Execute
        $result = $processor->create($toyCarModel);
        $this->assertTrue($result);
    }
    public function provideToyCarModel(): array
    {
        // Toy Car Color
        $toyColor = new ToyColor();
        $toyColor->setName(‘Black’);
        // Car Manufacturer
        $carManufacturer = new CarManufacturer();
        $carManufacturer->setName(‘Ford’);
        // Toy Car
        $toyCarModel = new ToyCar();
        $toyCarModel->setName(‘Mustang’);
        $toyCarModel->setColour($toyColor);
        $toyCarModel->setManufacturer
            ($carManufacturer);
        $toyCarModel->setYear(1968);
        return [
            [$toyCarModel],
        ];
    }
}

As you can see, we added a new Mock object for the validation. I will explain why we must do that after we refactor the content of the ToyCarCreator.php class. Let’s create a validator interface, and then refactor the ToyCarCreator class.

  1. Create the following file with the following content:

codebase/symfony/src/Validator/ValidatorInterface.php

<?php
namespace AppValidator;
interface ValidatorInterface
{
    /**
     * @param $input
     * @return bool
     * @throws ToyCarValidationException
     */
    public function validate($input): bool;
}
  1. Open codebase/symfony/src/Processor/ToyCarCreator.php and use the following content:
    <?php
    namespace AppProcessor;
    use AppDALWriterWriterInterface;
    use AppModelToyCar;
    use AppValidatorToyCarValidationException;
    use AppValidatorValidatorInterface;
    class ToyCarCreator
    {
        /**
         * @var ValidatorInterface
         */
        private $validator;
        /**
         * @var WriterInterface
         */
        private $dataWriter;
        /**
         * @param ToyCar $toyCar
         * @return bool
         * @throws ToyCarValidationException
         */
        public function create(ToyCar $toyCar): bool
        {
            // Do some validation here and so on...
            $this->getValidator()->validate($toyCar);
            // Write the data
            $result = $this->getDataWriter()->write
                ($toyCar);
            // Do other stuff.
            return $result;
        }
    }

Next, add the necessary accessors and mutators for the private properties we have declared in the class.

We renamed the class just to give it a more specific name. Sometimes, just naming the class to something else helps you clean up your code. Also, you will notice that we have removed the publicly visible validate class. This class will no longer contain any validation logic – it only knows that it will run a validation routine before it tries to persist the data. This is the class’s main responsibility.

We still have not written any validation and data persistence code, but let’s see whether we can still pass the test to test the main responsibility of the class, which is to do the following:

  1. Accept a ToyCar model object.
  2. Run a validation routine.
  3. Attempt to persist the data.
  4. Return the result.
  1. Run the following command:
    /var/www/html/symfony# ./vendor/bin/phpunit --filter ToyCarCreatorTest

Now, you should see the following result:

Figure 8.6 – Passing the test using two stubs

Figure 8.6 – Passing the test using two stubs

In this section, we used BDD and TDD to direct us into writing the solution code. We have created POPOs with a single responsibility. We have also created a ToyCarCreator class that does not contain the validation logic, nor the persistence mechanism. It knows it needs to do some validation and some persistence, but it does not have the concrete implementation of those programs. Each class will have its own specialization or a specific job, or a specific single responsibility.

Great – now, we can pass the test again even after refactoring. Next, let’s continue writing the solution code by following the O in the SOLID principle, which is the Open-Closed Principle (OCP).

TDD with the Open-Closed Principle

The OCP was first defined by Bertrand Meyer, but in this chapter, we will follow the later version defined by Robert C. Martin, which is also called the polymorphic OCP.

The OCP states that objects should be open to extension and closed to modification. The aim is that we should be able to modify the behaviour or a feature by extending the original code instead of directly refactoring the original code. That’s great because that will help us developers and testers be more confident about the ticket we’re working on, as we haven’t touched the original code that might be used somewhere else – less risk of regression.

In our ToyCarCreateTest class, we are stubbing a validator object because we have not written a concrete validator class yet. There are a lot of different ways of implementing validation, but for this example, we’ll try to make it very simple. Let’s go back to the code and create a validator:

  1. Create a new test class with the following content:

codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php

<?php
namespace AppTestsUnitValidator;
use AppModelCarManufacturer;
use AppModelToyCar;
use AppModelToyColor;
use AppValidatorToyCarValidator;
use PHPUnitFrameworkTestCase;
class ToyCarValidatorTest extends TestCase
{
    /**
     * @param ToyCar $toyCar
     * @param bool $expected
     * @dataProvider provideToyCarModel
     */
    public function testCanValidate(ToyCar $toyCar, 
        bool $expected): void
    {
        $validator  = new ToyCarValidator();
        $result     = $validator->validate($toyCar);
        $this->assertEquals($expected, $result);
    }
    public function provideToyCarModel(): array
    {
        // Toy Car Color
        $toyColor = new ToyColor();
        $toyColor->setName(‘White’);
        // Car Manufacturer
        $carManufacturer = new CarManufacturer();
        $carManufacturer->setName(‘Williams’);
        // Toy Car
        $toyCarModel = new ToyCar();
        $toyCarModel->setName(‘’); // Should fail.
        $toyCarModel->setColour($toyColor);
        $toyCarModel->setManufacturer
            ($carManufacturer);
        $toyCarModel->setYear(2004);
        return [
            [$toyCarModel, false],
        ];
    }
}

After creating the test class, as usual, we need to run the test to make sure that PHPUnit recognizes your test.

  1. Run the following command:
    /var/www/html/symfony# ./vendor/bin/phpunit --testsuite=Unit --filter ToyCarValidatorTest 

Make sure that you get an error, as we have not created the validator class yet. Remember the red phase? You’ll notice that in the data provider, we have set an empty string for the name. We will make the validator class return false whenever it sees an empty string for the toy car name.

  1. Now, that we have the failing test, let’s proceed with creating the class to pass it. Create a new PHP class with the following content:

codebase/symfony/src/Validator/ToyCarValidator.php

<?php
namespace AppValidator;
use AppModelToyCar;
class ToyCarValidator
{
    public function validate(ToyCar $toyCar): bool
    {
        if (!$toyCar->getName()) {
            return false;
        }
        return true;
    }
}

We have created a very simple validation logic where we only check for the toy car’s name if it’s not an empty string. Now, let’s run the test again.

  1. Run the following command:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter ToyCarValidatorTest

You should now see a passing test.

Okay, so for now, we can make sure that the toy car model’s name should always be a string that is not empty – but here’s the thing, what if we want to add more validation logic? We will have to keep on modifying the ToyCarValidator class. That’s not wrong. It’s just that it’s arguably better to follow the OCP so that we don’t keep modifying our code – less class modification, less risk of breaking things. Let’s refactor our solution code to pass the test again:

  1. Let’s add some validation logic for the year and retain the toy car name validation as well.
  2. Right now, we are in the green phase, moving to the refactor phase. We’ll be using polymorphism, which we discussed in Chapter 4, Using Object-Oriented Programming in PHP, instead of inheritance in this solution. Create the following interface with the following content:

codebase/symfony/src/Validator/ToyCarValidatorInterface.php

<?php
namespace AppValidator;
use AppModelToyCar;
use AppModelValidationModel;
interface ToyCarValidatorInterface
{
    public function validate(ToyCar $toyCar): 
        ValidationModel;
}
  1. We created a new ToyCarValidatorInterface interface that will replace the ToyCarValidator concrete class. You will notice that the validate method returns an object – let’s create that object too:

codebase/symfony/src/Model/ValidationModel.php

<?php
namespace AppModel;
class ValidationModel
{
    /**
     * @var bool
     */
    private $valid = false;
    /**
     * @var array
     */
    private $report = [];
}

After creating the class, generate the accessors and mutators for the properties.

Instead of simply returning true or false on our validation program, we can now return an array containing the field name and validation result for that field name as well. Let’s continue coding.

  1. Create the following test class with the following content:

codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php

<?php
namespace AppTestsUnitValidator;
use PHPUnitFrameworkTestCase;
use AppValidatorYearValidator;
class YearValidatorTest extends TestCase
{
    /**
     * @param $data
     * @param $expected
     * @dataProvider provideYear
     */
    public function testCanValidateYear(int $year, 
        bool $expected): void
    {
        $validator  = new YearValidator();
        $isValid    = $validator->validate($year);
        $this->assertEquals($expected, $isValid);
    }
    /**
     * @return array
     */
    public function provideYear(): array
    {
        return [
            [1,     false],
            [2005,  true],
            [1955,  true],
            [312,   false],
        ];
    }
}
  1. If you run this test, you will see four failures, as we have four sets of values inside the provideYear data provider. Run the test by running the following command:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter YearValidatorTest --debug

If the test fails, that’s good. Let’s proceed with the solution code:

  1. Create the following solution class with the following content:

codebase/symfony/src/Validator/YearValidator.php

<?php
namespace AppValidator;
class YearValidator implements ValidatorInterface
{
    /**
     * @param $input
     * @return bool
     */
    public function validate($input): bool
    {
        if (preg_match(“/^(d{4})$/”, $input, 
            $matches)) {
            return true;
        }
        return false;
    }
}

Now, we have a simple validation class for checking whether a year is acceptable for our car. If we want to add more logic here, such as checking for the minimum and maximum acceptable value, we can put all that logic here.

  1. Run the following command again and see whether the tests pass:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter YearValidatorTest --debug

You should see the following result:

Figure 8.7 – Simple date validation test

Figure 8.7 – Simple date validation test

Now that we have passed the very simple test for the year validator, next, let’s move on to the name validator:

  1. Create the following test class with the following content:

codebase/symfony/tests/Unit/Validator/NameValidatorTest.php

<?php
namespace AppTestsUnitValidator;
use AppValidatorNameValidator;
use PHPUnitFrameworkTestCase;
class NameValidatorTest extends TestCase
{
    /**
     * @param $data
     * @param $expected
     * @dataProvider provideNames
     */
    public function testCanValidateName(string $name, 
        bool $expected): void
    {
        $validator  = new NameValidator();
        $isValid    = $validator->validate($name);
        $this->assertEquals($expected, $isValid);
    }
    /**
     * @return array
     */
    public function provideNames(): array
    {
        return [
            [‘’,            false],
            [‘$50’,         false],
            [‘Mercedes’,    true],
            [‘RedBull’,     true],
            [‘Williams’,    true],
        ];
    }
}
  1. As with the year validator, if you run this test now, you will encounter multiple errors, but we have to make sure that it does fail or error out. Run the following command:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter NameValidatorTest
  2. After running the command, you should see five errors. That’s okay. Let’s build the solution code for it now. Create the following class with the following content:

codebase/symfony/src/Validator/NameValidator.php

<?php
namespace AppValidator;
class NameValidator implements ValidatorInterface
{
    public function validate($input): bool
    {
        if (preg_match(“/^([a-zA-Z’ ]+)$/”, $input)) {
            return true;
        }
        return false;
    }
}
  1. Now, we have a simple logic to validate a name. Let’s run the name validator test again, and see whether it passes. Run the following command again:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter NameValidatorTest

You should now see five passing tests.

Let’s summarize what we have added so far. We created two new validation classes, and both are working as expected based on our unit tests – but how is this better than the first solution we created? How is this relevant to the OCP? Well, first we need to tie things together and pass the bigger ToyCarValidatorTest.

  1. Let’s refactor the ToyCarValidator class with the following content:

codebase/symfony/src/Validator/ToyCarValidator.php

<?php
namespace AppValidator;
use AppModelToyCar;
use AppModelValidationModel as ValidationResult;
class ToyCarValidator implements 
    ToyCarValidatorInterface
{
    /**
     * @var array
     */
    private $validators = [];
    public function __construct()
    {
        $this->setValidators([
            ‘year’  => new YearValidator(),
            ‘name’  => new NameValidator(),
        ]);
    }
    /**
     * @param ToyCar $toyCar
     * @return ValidationResult
     */
    public function validate(ToyCar $toyCar
        ValidationResult
    {
        $result     = new ValidationResult();
        $allValid   = true;
        foreach ($this->getValidators() as $key => 
            $validator) {
            $accessor   = ‘get’ . ucfirst(strtolower
                ($key));
            $value      = $toyCar->$accessor();
            $isValid    = false;
            try {
                $isValid = $validator->validate
                    ($value);
                $results[$key][‘message’]   = ‘’;
            } catch (ToyCarValidationException $ex) {
                $results[$key][‘message’]   = $ex->
                    getMessage();
            } finally {
                $results[$key][‘is_valid’]  = 
                     $isValid;
            }
            if (!$isValid) {
                $allValid = false;
            }
        }
        $result->setValid($allValid);
        $result->setReport($results);
        return $result;
    }
}

Then, generate the accessors and mutators for the $validators property.

  1. You will notice that in the constructor, we are instantiating two validator classes, and within the validate method, we are using those validator classes. Each validator class will then have its own custom logic on how to run the validate method. Now, refactor the following test class with the following content:

codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php

<?php
namespace AppTestsUnitValidator;
use AppModelCarManufacturer;
use AppModelToyCar;
use AppModelToyColor;
use AppValidatorToyCarValidator;
use PHPUnitFrameworkTestCase;
class ToyCarValidatorTest extends TestCase
{
    /**
     * @param ToyCar $toyCar
     * @param array $expected
     * @dataProvider provideToyCarModel
     */
    public function testCanValidate(ToyCar $toyCar, 
        array $expected): void
    {
        $validator  = new ToyCarValidator();
        $result     = $validator->validate($toyCar);
        $this->assertEquals($expected[‘is_valid’], 
            $result->isValid());
        $this->assertEquals($expected[‘name’], 
            $result->getReport()[‘name’][‘is_valid’]);
        $this->assertEquals($expected[‘year’], 
            $result->getReport()[‘year’][‘is_valid’]);
    }
    public function provideToyCarModel(): array
    {
        // Toy Car Color
        $toyColor = new ToyColor();
        $toyColor->setName(‘White’);
        // Car Manufacturer
        $carManufacturer = new CarManufacturer();
        $carManufacturer->setName(‘Williams’);
        // Toy Car
        $toyCarModel = new ToyCar();
        $toyCarModel->setName(‘’); // Should fail.
        $toyCarModel->setColour($toyColor);
        $toyCarModel->setManufacturer
            ($carManufacturer);
        $toyCarModel->setYear(2004);
        return [
            [$toyCarModel, [‘is_valid’ => false, 
                ‘name’ => false, ‘year’ => true]],
        ];
    }
}
  1. Now, in this test, we are checking for the validity of the entire toy car model object, as well as checking which specific field of the toy car model has passed or failed the validation. Let’s see whether the test passes. Run the following command:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter ToyCarValidatorTest

Now, you should see the following result:

Figure 8.8 – Passing toy car validation test

Figure 8.8 – Passing toy car validation test

You will notice that we passed three assertions. It looks like we are starting to get a test with more responsibilities. It’s still better to do one assertion per test, just so that we don’t end up having a god test class! For now, we’ll move on.

Now, what have we achieved by refactoring? Well, first, we no longer have the validation logic for checking the validity of the toy name inside the ToyCarValidatorTest class. Second, we can now check for the validity of the year. If we want to improve the date and name validation logic, we won’t have to do it in the main ToyCarValidator class – but what if we want to add more validator classes? Such as a ToyColorValidator class? Well, we can still do that without even touching the main class! We’ll refactor ToyCarValidator and discuss how to do so later in the chapter in the TDD with the Dependency Inversion Principle section.

But what if we want to change the entire behavior of the ToyCarValidator.php class we created and change the logic entirely? Well, there’s no need to modify it – we can just replace the entire ToyCarValidator.php class with a different concrete implementation of the ToyCarValidatorInterface interface!

Next, we’ll talk about the Liskov Substitution Principle (LSP).

TDD with the Liskov Substitution Principle

The LSP was introduced by Barbara Liskov. The way that I use it is that an implementation of an interface should be replaceable with another implementation of that interface without changing the behavior. If you are extending a superclass, the child class must be able to substitute the superclass without breaking the behavior.

In this example, let’s try adding a business rule to reject toy car models that were built on or before 1950.

As usual, let’s start with a test:

  1. Open the YearValidatorTest.php class we created earlier and modify the test class with the following:

codebase/symfony/tests/Unit/Validator/YearValidatorTest.php

<?php
namespace AppTestsUnitValidator;
use AppValidatorToyCarTooOldException;
use PHPUnitFrameworkTestCase;
use AppValidatorYearValidator;
class YearValidatorTest extends TestCase
{
    /**
     * @param $data
     * @param $expected
     * @dataProvider provideYear
     */
    public function testCanValidateYear(int $year, 
        bool $expected): void
    {
        $validator  = new YearValidator();
        $isValid    = $validator->validate($year);
        $this->assertEquals($expected, $isValid);
    }
    /**
     * @return array
     */
    public function provideYear(): array
    {
        return [
            [1,     false],
            [2005,  true],
            [1955,  true],
            [312,   false],
        ];
    }
    /**
     * @param int $year
     * @dataProvider provideOldYears
     */
    public function testCanRejectVeryOldCar(int 
        $year): void
    {
        $this->expectException
            (ToyCarTooOldException::class);
        $validator  = new YearValidator();
        $validator->validate($year);
    }
    /**
     * @return array
     */
    public function provideOldYears(): array
    {
        return [
            [1944],
            [1933],
            [1922],
            [1911],
        ];
    }
}
  1. We added a new test so that we check for ToyCarTooOldException. Let’s add this exception class as well, but first, let’s run the test.
  2. Run the following command:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar
  3. Now you will see four errors. That’s okay. Now, let’s add the missing exception class:

codebase/symfony/src/Validator/ToyCarTooOldException.php

<?php
namespace AppValidator;
class ToyCarTooOldException extends Exception
{
}

As you can see, it’s just a simple exception class that extends the main PHP Exception class.

If we run the test again, we should now pass the test, as we have told PHPUnit that we are expecting exceptions for this test by using the $this->expectException() method.

  1. Run the following command:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanRejectVeryOldCar

Now, we should be able to pass the test – you should see the following result:

Figure 8.9 – Passing the old car rejection test

Figure 8.9 – Passing the old car rejection test

This means that we are correctly throwing the ToyCarTooOldException object whenever we submit a year that is less than or equal to 1950 – but what will happen to our ToyCarValidatorTest?

Let’s modify the test data with a year less than 1950 and see what happens:

  1. Modify the data provider content with the following:

codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php

public function provideToyCarModel(): array
{
    // Toy Car Color
    $toyColor = new ToyColor();
    $toyColor->setName(‘White’);
    // Car Manufacturer
    $carManufacturer = new CarManufacturer();
    $carManufacturer->setName(‘Williams’);
    // Toy Car
    $toyCarModel = new ToyCar();
    $toyCarModel->setName(‘’); // Should fail.
    $toyCarModel->setColour($toyColor);
    $toyCarModel->setManufacturer($carManufacturer);
    $toyCarModel->setYear(1935);
    return [
        [$toyCarModel, [‘is_valid’ => false, ‘name’ => 
            false, ‘year’ => false]],
    ];
}
  1. Now, run the following command and see what happens:
    /var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest 

You will notice that we have failed the test with the following message:

Figure 8.10 – Failed toy car validation

Figure 8.10 – Failed toy car validation

Now, we can see that we have an uncaught exception. Our ToyCarValidator program is not programmed to handle this exception object. Why is that? Well, the interface in this example is the codebase/symfony/src/Validator/ValidatorInterface.php interface. This interface throws a ToyCarValidationException object. The problem now is that our implementing class, the YearValidator.php class, throws a different exception compared to its contract or interface. Therefore, it breaks the behavior. To fix this problem, we simply need to throw the correct exception as declared in the interface.

  1. Let’s modify the ToyCarTooOldException class:

codebase/symfony/src/Validator/ToyCarTooOldException.php

<?php
namespace AppValidator;
class ToyCarTooOldException extends 
    ToyCarValidationException
{
}

As you can see, we simply replaced the class it extends to ToyCarValidationException. The ToyCarValidator.php class is designed to catch this exception.

  1. Now, let’s run the test by running the following command and see whether it really works:
    /var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest

We should now pass the test and see the following result:

Figure 8.11 – Passing the toy car validator test, with old car validation

Figure 8.11 – Passing the toy car validator test, with old car validation

  1. Now that we are passing the test again, let’s see what is being returned by our ToyCarValidator program. Remember the shell scripts we wrote back in Chapter 5, Unit Testing? Let’s use one of them. Put a breakpoint in codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php at line 23. Then, run the following command:
    /var/www/html/symfony# ./runDebug.sh --filter ToyCarValidatorTest
  2. Inspect the $result variable, and you should see the following content:
Figure 8.12 – Validation model

Figure 8.12 – Validation model

You can see that our ToyCarValidator’s validate method returns a ValidationModel object. It gives a summary of the fields we validated for, as well as the exception message for the year field.

We’ve seen how interfaces can be useful, but sometimes they become too powerful. Next, we’ll talk about the Interface Segregation Principle (ISP) to help stop this from happening.

TDD with the Interface Segregation Principle

Interfaces are very helpful, but sometimes it can be very easy to pollute them with capabilities that are not really supposed to be a part of the interface. I used to encounter this violation a lot. I was asking myself how I kept on creating empty methods with to-do comments, only to find classes a few months or years later, still with those to-do comments and the methods still empty.

I used to touch my interfaces first and stuff them with all the methods I thought I needed. Then, when I finally wrote the concrete implementations, these concrete classes mostly had empty methods in them.

An interface should only have methods that are specific to that interface. If there’s a method in there that is not entirely related to that interface, you need to segregate it into a different interface.

Let’s see that in action. Again, let’s start with a – you guessed it right – test:

  1. Open the codebase/symfony/tests/Unit/Validator/NameValidatorTest.php test class and add the following content:
    /**
     * @param $data
     * @param $expected
     * @dataProvider provideLongNames
     */
    public function testCanValidateNameLength(string 
        $name, bool $expected): void
    {
        $validator  = new NameValidator();
        $isValid    = $validator->validateLength($name);
        $this->assertEquals($expected, $isValid);
    }
    /**
     * @return array
     */
    public function provideLongNames(): array
    {
        return [
            [‘TheQuickBrownFoxJumpsOverTheLazyDog’, 
                false],
        ];
    }

We introduced a new function in the test called validateLength, which is common for strings. We also added a very long name, and we set false to be expected to be returned in the data provider.

  1. Run the following test:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength

You should get an error, as we have not created the new method yet.

  1. Now, open the ValidatorInterface.php interface and add the validateLength method we are expecting to have in our test:

codebase/symfony/src/Validator/ValidatorInterface.php

<?php
namespace AppValidator;
interface ValidatorInterface
{
    /**
     * @param $input
     * @return bool
     * @throws ToyCarValidationException
     */
    public function validate($input): bool;
    /**
     * @param string $input
     * @return bool
     */
    public function validateLength(string $input): 
        bool;
}
  1. Great – now we have the contract for validating a string’s length. If we go back to the NameValidator.php class, we’ll get the following error from the IDE:
Figure 8.13 – Must implement the method

Figure 8.13 – Must implement the method

Obviously, we need to implement the validateLength method for the NameValidator.php class, which is okay, as we want to validate the string length – but what would happen if we also wanted to create a validator for the ToyCar model’s color? The ToyCar model’s color property expects a ToyColor.php object, not a string! Therefore, the solution is to delete the validateLength method from ValidatorInterface. Certain classes will implement ValidatorInterface without the need to implement this logic. What we can do instead is create a new interface called the StringValidator interface that can have the validateLength method.

  1. Refactor the codebase/symfony/src/Validator/ValidatorInterface.php interface and delete the validateLength method we just added, and create the following file with the following content:

codebase/symfony/src/Validator/StringValidatorInterface.php

<?php
namespace AppValidator;
interface StringValidatorInterface
{
    /**
     * @param string $input
     * @return bool
     */
    public function validateLength(string $input): 
        bool;
}

At this stage, we have segregated the validateLength method into a separate interface, removing it from the ValidatorInterface.php interface.

  1. Now, open the NameValidator.php class, and refactor it with the following content:

codebase/symfony/src/Validator/NameValidator.php

<?php
namespace AppValidator;
class NameValidator implements ValidatorInterface, 
    StringValidatorInterface
{
    const MAX_LENGTH = 10;
    /**
     * @param $input
     * @return bool
     */
    public function validate($input): bool
    {
        $isValid = false;
        if (preg_match(“/^([a-zA-Z’ ]+)$/”, $input)) {
            $isValid = true;
        }
        if ($isValid) {
            $isValid = $this->validateLength($input);
        }
        return $isValid;
    }
    /**
     * @param string $input
     * @return bool
     */
    public function validateLength(string $input): 
        bool
    {
        if (strlen($input) > self::MAX_LENGTH) {
            return false;
        }
        return true;
    }
}
  1. We have refactored the NameValidator class so that it now also checks for the name’s length. Let’s run the test and see whether it passes:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength

Now, you should see the following result:

Figure 8.14 – Passing the string length validation test

Figure 8.14 – Passing the string length validation test

What we did is instead of combining different methods into ValidatorInterface, we segregated them into two different interfaces. Then, we only implement the StringValidator interface for the validator objects that will need this validateLength method. That’s basically what the ISP is all about. This is a very basic example, but it is very easy to fall victim to these very powerful interfaces if you don’t watch out.

Next, we will go back to the ToyCarValidator class and see how we can improve what we had earlier in the TDD with the Open-Closed Principle example, using the Dependency Inversion Principle (DIP).

TDD with the Dependency Inversion Principle

In terms of making a class more testable, the DIP is probably the most important principle on the list for me. The DIP suggests that details should depend on abstractions. To me, this means that the specifics of a program that does not really belong to a class should be abstracted. The DIP allows us as developers to remove a concrete implementation of a routine or program and put it in a different object altogether. We can then use the DIP to inject the object that we need, whenever we need it. We can inject the object that we need in the constructor, passed as an argument upon class instantiation, or simply expose a mutator function.

Let’s revisit the ToyCarValidator class that we created earlier in this chapter to see how we can implement the DIP.

How will this look in our code?

Going back to the ToyCarValidator.php class, you will notice that in the __constructor method, we have instantiated two classes:

Figure 8.15 – Hardcoded dependencies

Figure 8.15 – Hardcoded dependencies

How can we improve this? Well, this program works – as you have seen, we are passing ToyCarValidatorTest. The only problem is that our ToyCarValidator class is now hardcoded to its dependencies – the YearValidator and NameValidator classes. What if we want to replace these classes – or what if we want to add more validators? Well, what we can do is remove the dependency from inside of the class. Follow these steps:

  1. Refactor the following test class, and replace the testCanValidate method with the following content:

codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php

/**
 * @param ToyCar $toyCar
 * @param array $expected
 * @dataProvider provideToyCarModel
 */
public function testCanValidate(ToyCar $toyCar, array 
    $expected): void
{
    $validators = [
        ‘year’  => new YearValidator(),
        ‘name’  => new NameValidator(),
    ];
    // Inject the validators
    $validator = new ToyCarValidator();
    $validator->setValidators($validators);
    $result = $validator->validate($toyCar);
    $this->assertEquals($expected[‘is_valid’], 
        $result->isValid());
    $this->assertEquals($expected[‘name’], 
        $result->getReport()[‘name’][‘is_valid’]);
    $this->assertEquals($expected[‘year’], 
        $result->getReport()[‘year’][‘is_valid’]);
}

You will notice that the objects that ToyCarValidator depends on are now being instantiated outside the ToyCarValidator class – and we then set the validators using the setValidators mutator.

  1. Now, remove the hardcoded validator instantiations from the ToyCarValidator’s constructor:

codebase/symfony/src/Validator/ToyCarValidator.php

<?php
namespace AppValidator;
use AppModelToyCar;
use AppModelValidationModel as ValidationResult;
class ToyCarValidator implements 
    ToyCarValidatorInterface
{
    /**
     * @var array
     */
    private $validators = [];
    /**
     * @param ToyCar $toyCar
     * @return ValidationResult
     */
    public function validate(ToyCar $toyCar): 
        ValidationResult
    {
        $result     = new ValidationResult();
        $allValid   = true;
        $results    = [];
        foreach ($this->getValidators() as $key => 
            $validator) {
            $accessor   = ‘get’ . ucfirst(strtolower
                ($key));
            $value      = $toyCar->$accessor();
            $isValid    = false;
            try {
                $isValid = $validator->validate
                    ($value);
                $results[$key][‘message’]   = ‘’;
            } catch (ToyCarValidationException $ex) {
                $results[$key][‘message’]   = 
                    $ex->getMessage();
            } finally {
                $results[$key][‘is_valid’]  = 
                    $isValid;
            }
            if (!$isValid) {
                $allValid = false;
            }
        }
        $result->setValid($allValid);
        $result->setReport($results);
        return $result;
    }
    /**
     * @return array
     */
    public function getValidators(): array
    {
        return $this->validators;
    }
    /**
     * @param array $validators
     */
    public function setValidators(array $validators): 
        void
    {
        $this->validators = $validators;
    }
}
  1. We no longer have the hardcoded validator instantiations – now, let’s run the test and see whether the tests still pass:
    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength

After running the command, you should see that the tests still pass. At this point, we can keep creating new validators and just add them to the array of validators we want to inject into the ToyCarValidator.php class.

  1. Now, open the ToyCarCreator.php class we created earlier in this chapter, and you’ll see that it’s already prepared to accept dependencies from the outside. We can also refactor the class so that we can automatically inject the dependencies it needs during instantiation.
  2. Open the following test class and refactor it with the following content:

codebase/symfony/tests/Integration/Processor/ToyCarCreatorTest.php

<?php
namespace AppTestsIntegrationRepository;
use AppDALWriterWriterInterface;
use AppModelCarManufacturer;
use AppModelToyCar;
use AppModelToyColor;
use AppModelValidationModel;
use AppProcessorToyCarCreator;
use AppValidatorToyCarValidatorInterface;
use SymfonyBundleFrameworkBundleTestKernelTestCase;
class ToyCarCreatorTest extends KernelTestCase
{
    /**
     * @param ToyCar $toyCarModel
     * @throws AppValidator
           ToyCarValidationException
     * @dataProvider provideToyCarModel
     */
    public function testCanCreate(ToyCar 
        $toyCarModel): void
    {
        $validationResultStub = $this->createMock
            (ValidationModel::class);
        $validationResultStub
            ->method(‘isValid’)
            ->willReturn(true);
        // Mock 1: Validator
        $validatorStub = $this->createMock
            (ToyCarValidatorInterface::class);
        $validatorStub
            ->method(‘validate’)
            ->willReturn($validationResultStub);
        // Mock 2: Data writer
        $toyWriterStub = $this->createMock
            (WriterInterface::class);
        $toyWriterStub
            ->method(‘write’)
            ->willReturn(true);
        // Processor Class
        $processor = new ToyCarCreator($validatorStub, 
            $toyWriterStub);
        // Execute
        $result = $processor->create($toyCarModel);
        $this->assertTrue($result);
    }
    public function provideToyCarModel(): array
    {
        // Toy Car Color
        $toyColor = new ToyColor();
        $toyColor->setName(‘Black’);
        // Car Manufacturer
        $carManufacturer = new CarManufacturer();
        $carManufacturer->setName(‘Ford’);
        // Toy Car
        $toyCarModel = new ToyCar();
        $toyCarModel->setName(‘Mustang’);
        $toyCarModel->setColour($toyColor);
        $toyCarModel->setManufacturer
            ($carManufacturer);
        $toyCarModel->setYear(1968);
        return [
            [$toyCarModel],
        ];
    }
}

As you can see, we have instantiated the dependencies of the ToyCarCreator.php class and then injected them as a parameter when we instantiated the class in ToyCarCreator($validatorStub, $toyWriterStub);.

  1. Then, open the ToyCarCreator.php solution class and refactor it with the following content:

codebase/symfony/src/Processor/ToyCarCreator.php

<?php
namespace AppProcessor;
use AppDALWriterWriterInterface;
use AppModelToyCar;
use AppValidatorToyCarValidationException;
use AppValidatorToyCarValidatorInterface;
class ToyCarCreator
{
    /**
     * @var ToyCarValidatorInterface
     */
    private $validator;
    /**
     * @var WriterInterface
     */
    private $dataWriter;
    public function __construct
        (ToyCarValidatorInterface $validator, 
            WriterInterface $dataWriter)
    {
        $this->setValidator($validator);
        $this->setDataWriter($dataWriter);
    }
    /**
     * @param ToyCar $toyCar
     * @return bool
     * @throws ToyCarValidationException
     */
    public function create(ToyCar $toyCar): bool
    {
        // Do some validation here and so on...
        $this->getValidator()->validate($toyCar);
        // Write the data
        $result = $this->getDataWriter()->write
            ($toyCar);
        // Do other stuff.
        return $result;
    }
    /**
     * @return WriterInterface
     */
    public function getDataWriter(): WriterInterface
    {
        return $this->dataWriter;
    }
    /**
     * @param WriterInterface $dataWriter
     */
    public function setDataWriter(WriterInterface 
        $dataWriter): void
    {
        $this->dataWriter = $dataWriter;
    }
    /**
     * @return ToyCarValidatorInterface
     */
    public function getValidator(): 
        ToyCarValidatorInterface
    {
        return $this->validator;
    }
    /**
     * @param ToyCarValidatorInterface $validator
     */
    public function setValidator
        (ToyCarValidatorInterface $validator): void
    {
        $this->validator = $validator;
    }
}

Upon instantiation, both the validator and writer dependencies are set through the constructor.

If we run the test, it should still pass:

/var/www/html/symfony# ./runDebug.sh --testsuite=Integration --filter ToyCarCreatorTest

After running the command, you should still see a passing test.

The most obvious thing that you will notice with this approach is that you will have to manage all the dependencies yourself and then inject them into the object that needs them. Luckily, we are not the first people to encounter this headache. There are a lot of service containers out there that help manage the dependencies that your application needs, but the most important thing when selecting a service container for PHP is that it should follow the PSR-11 standards. You can read more about PSR-11 at https://www.php-fig.org/psr/psr-11/.

Summary

In this chapter, we’ve gone through the SOLID principles one by one. We used our tests to kickstart the development of our solution code so that we can use them as examples for implementing the SOLID principles in real life.

We have covered the SRP, which helped us make a PHP class’s responsibility or capability more focused. The OCP helped us avoid the need for touching or modifying a class in some instances when we want to change its behavior. The LSP helped us be stricter about the behavior of an interface, making it easier for us to switch concrete objects implementing that interface without breaking the parent class’s behavior. The ISP helped us make the responsibility of an interface more focused – classes that implement this interface will no longer have empty methods just because they were declared by the interface. The DIP helped us quickly test our ToyCarCreator class even without creating a concrete implementation of its dependencies, such as the ToyCarValidator class.

When working on real-life projects, some principles are hard to strictly follow, and sometimes the boundaries are vague. Add the pressure of real-life deadlines and it gets even more interesting. One thing is for sure, using BDD and TDD will help you be more confident about the features you are developing, especially when you are already a few months deep into a project. Adding SOLID principles on top of that makes your solution even better!

In the next chapter, we will try to utilize automated tests to help us make sure that any code changes that any developer in your team pushes into your code repository will not break the expected behavior of your software. We will try to automate this process by using Continuous Integration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.238.70