Programmers: TDD your Legacy Code to Single Respnsibility Principle. Part 3 – Dependency Injection For the New Class and Wrap Up

TL;DR

This article series shows the importance of having unit tests. It also shows the importance of adhering to the Single Responsibility Principle (SRP), as we get a new requirement for our code base. We take a section of “legacy code” and refactor it to more closely follow SRP, all while maintaining the safety net of unit tests and leveraging Test Driven Development (TDD). We also cover using Dependency Injection in this TDD context.

This was originally going to be one post, but ended up WAY too long, so it will be a series of three posts:
Part 1 – Setup and Planning the Responsibility Transfer
Part 2 – The Glorious TDD Cycle
Part 3 – Dependency Injection For the New Class and Wrap Up

Please following along on GitHub. The name of the downloaded .zip file for various links in this article correspond to commits in this Github repo.

In part 2 of the series, we finished the QuestionAndAnswerValidator class, and its unit tests. In this part, we’ll use Dependency Injection to make the GuessForm class aware of the new QuestionAndAnswerValidator class, as well as use TDD when modifying the GuessForm::validateForm method to utilize QuestionAndAnswerValidator.

Making the Form Aware Of New Validator Class – Dependency Injection

Now that we have the answer validation logic extracted to a new class, and that the new class is covered with unit tests, we can go to work removing that logic from the GuessForm class. To keep the tests passing at all times, we first need to provide and utilize a QuestionAndAnswerValidator object in the GuessForm class. For this, we’ll leverage the dependency injection (DI) container that the Symphony framework provides within Drupal 8.


The goal of DI is to provide services to a client class, without that client class ever knowing the exact implementation that is providing the services. In our case, there are essentially 3 steps to leverage DI.

First, in order for the client (GuessForm) to never know the exact implementation providing the answer validation services, we create a QuestionAndAnswerValidatorInterface interface containing the single method that GuessForm will call:

interface QuestionAndAnswerValidatorInterface
{
  public function validateAnswerToQuestion($question_key, $answer_to_validate);
}

We also update the class declaration of QuestionAndAnswerValidator to advertise the fact that it implements QuestionAndAnswerValidatorInterface:

class QuestionAndAnswerValidator implements QuestionAndAnswerValidatorInterface

Second, we need to make it known to the world (or, at least to the Symphony DI container) that the QuestionAndAnswerValidator is available to be injected as a dependency. This is done by creating a file called form_validation_module_refactor_srp.services.yml (or, whatever your module is named) in the module root directory. The file will contain an entry for the service we are exposing, which just says that the “question_and_answer_validator” service is really provided by the QuestionAndAnswerValidator implementation.

services:
  question_and_answer_validator:
    class: Drupal\form_validation_module_refactor_srp\Model\QuestionAndAnswerValidator

Third/last for setting up the DI, the GuessForm needs to tell the Symphony DI container that it needs the “question_and_answer_validator” service. Doing this will require a few edits to the GuessForm class. We need to add a “use” statement at the top of the file:

use Drupal\form_validation_module_refactor_srp\Model\QuestionAndAnswerValidatorInterface;

We also need to add a protected property to store the instance of QuestionAndAnswerValidatorInterface that will be injected into the GuessForm class. We add a constructor that accepts an instance of QuestionAndAnswerValidatorInterface and sets it to the newly created protected property. Last, we implement the ContainerInjectionInterface::create method. The abstract FormBase class from which GuessForm inherits actually declares conformance to the ContainerInjectionInterface interface. However, our specific GuessForm implementation overrides that create method to suit our specific needs. The ContainerInjectionInterface::create method just tells the Symphony dependency injection container what dependency to inject into GuessForm:

  
protected $question_and_answer_validator;

public function __construct(QuestionAndAnswerValidatorInterface $question_and_answer_validator) 
{
  $this->question_and_answer_validator = $question_and_answer_validator;
}

public static function create(ContainerInterface $container) 
{
  return new static(
    $container->get('question_and_answer_validator')
  );
}

So, now everything is wired up. The “question_and_answer_validator” service defined in the services.yml file is instantiated by the dependency injection container and provided to the GuessForm via the GuessForm’s constructor, and the service is saved as a property on GuessForm for later use. The GuessForm object has no knowledge of the specific implementation of the QuestionAndAnswerValidatorInterface that is provided by the dependency injection container. At a later point in time, we could change the services.yml file to provide a different class, and so long as it implements the QuestionAndAnswerValidatorInterface::validateAnswerToQuestion method, the GuessForm does not care — nor does it’s code need to change! The step we just accomplished of setting up dependency injection is reflected in this repository commit.

Updating the Original GuessForm Unit Tests

We’ve provided the QuestionAndAnswerValidatorInterface implementation to GuessForm, and we’ve constructed the unit tests for QuestionAndAnswerValidator all along in this article. Now we need to update the unit tests that were originally for GuessForm to reflect this shift in responsibility, prior to actually making the production code updates.

Before we do the updating, we will want to remove some of the original unit tests that were on GuessForm. GuessForm will only now be responsible for validation responsibilities 2 and 3 (see the “Planning Our Extraction / Original Responsibilities” section of part 1). So, we will remove the following unit test methods on the GuessFormValidateMethodTestCase class: testFormValidationNoAnswerGiven, testFormValidationIncorrectAnswerDetectedWhenIncorrectAnswerEntered, and testFormValidationCorrectAnswerGivenForNotCurrentlySelectedQuestion. The only unit test methods for validation errors that remain on GuessFormValidateMethodTestCase are testFormValidationNoQuestionSelected and testFormValidationDefaultTextFieldNotChanged. The reason to remove those three unit tests is that they are redundant (we already test those cases on QuestionAndAnswerValidatorTestCase), and would only be unnecessary maintenance down the road.

After that removal, the main difference we have to account for is to provide a mock QuestionAndAnswerValidator to the GuessForm test object we use in the remaining unit tests. To keep the code generic and continue with our usage of the FormValidationUnitTestCase superclass (see previous article), we will update the superclass to allow for generic passing of ad hoc parameters to the constructor of the Form object that is being created for the unit tests. Here’s how…

There’s a method within the PHPUnit framework called setConstructorArgs(), which passes the given arguments to the constructor of the mock object that is being created. Now, we don’t want to disable the constructor of the mock FormBase object that is being created by FormValidationUnitTestCase::getMockedFormObject (as we did before), but rather we want inject dependencies into the FormBase object via it’s constructor. We update the applicable line of FormValidationUnitTestCase::getMockedFormObject to read as follows:

    
$mock_form = $this->getMockBuilder($fully_namespaced_form_class_to_test)
  ->setMethods(null)
  ->setConstructorArgs($this->getFormConstructorArguments())
  ->getMock();

The constructor args are retrieved from the FormValidationUnitTestCase::getFormConstructorArguments method, which is a new abstract method on FormValidationUnitTestCase. The implementation of that method on the GuessFormValidateMethodTestCase (which subclasses from FormValidationUnitTestCase) looks simply like this:

  
protected function getFormConstructorArguments()
{
  return [ $this->mock_question_and_answer_validator ];
}

Lastly, the mock_question_and_answer_validator property of GuessFormValidateMethodTestCase is set by a newly implemented setUp() method. This method is called by thePHPUnit framework prior to each test method that executes. We’ve implemented it just now in order to setup a mock QuestionAndAnswerValidator object, which has all of it’s methods replaced with stubs (not shown).

Of the remaining unit tests on GuessFormValidateMethodTestCase, testFormValidationNoQuestionSelected and testFormValidationDefaultTextFieldNotChanged are unchanged, and we will update testFormValidationNoErrorThrownForCorrectInput to expect the new call to QuestionAndAnswerValidator::validateAnswerToQuestion and also mock it’s response — this covers the case where validateAnswerToQuestion returns true for the correct answer to a question:

  
public function testFormValidationNoErrorThrownForCorrectInput() 
{
  $form_element_names_and_input_values = [
    GuessForm::select_list_key => GuessForm::favorite_aircraft_make_key,
    GuessForm::text_field_key => 'Cessna'      
  ];

  $this->mock_question_and_answer_validator->expects($this->once())
    ->method('validateAnswerToQuestion')
      ->with(GuessForm::favorite_aircraft_make_key, 'Cessna')
        ->will($this->returnValue(true));

  $this->assertFormElementDoesNotCausesErrorMessageWithValidFormInput($form_element_names_and_input_values);
}

Lastly we actually need to implement a new unit test on GuessFormValidateMethodTestCase, to cover the case where QuestionAndAnswerValidator::validateAnswerToQuestion returns an error message. The real QuestionAndAnswerValidator class can return 3 different possible error messages, and we’ve already covered those cases with unit tests on the QuestionAndAnswerValidator itself. From the perspective of unit testing the GuessForm class, however, we simply need to check that ANY error message flows through from the mock QuestionAndAnswerValidator to GuessForm to the mock FormStateInterface object (hence why we just use a generic “This is an error message” as the error message!):

  
public function testFormValidationCallsSetErrorByNameWhenValidatorReturnsError() {
  $form_element_names_and_input_values = [
    GuessForm::select_list_key => GuessForm::favorite_aircraft_make_key,
    GuessForm::text_field_key => 'wrong answer'      
  ];

  $this->mock_question_and_answer_validator->expects($this->once())
    ->method('validateAnswerToQuestion')
    ->with(GuessForm::favorite_aircraft_make_key, 'wrong answer')
    ->will($this->returnValue("This is an error message"));

  $expected_error_message = "This is an error message";

  $this->assertFormElementCausesErrorMessageWithInvalidFormInput(GuessForm::text_field_key, $expected_error_message, $form_element_names_and_input_values);  
}

At Last! Transferring Responsibility From GuessForm to QuestionAndAnswerValidator

Now that we have the QuestionAndAnswerValidatorInterface implementation provided to GuessForm and unit tests to safely cover our move, we can proceed with altering the GuessForm::validateForm method to not do it’s own validation, but instead rely on the QuestionAndAnswerValidatorInterface implementation.

If we run the unit tests right now (see this commit), we would be greeted with 2 failures, one for the unit test we added to GuessForm, and the other one we updated. To complete the TDD cycle, we now write the production code to make the test pass. This change largely simplifies the GuessForm::validateForm method, and simply delegates more responsibility to the newly injected QuestionAndAnswerValidator dependency:

  
public function validateForm(array &$form, FormStateInterface $form_state)
{
  $values = $form_state->getValues();

  $selected_question_key = $values[GuessForm::select_list_key];
  $entered_answer_value = $values[GuessForm::text_field_key];

  if ($selected_question_key == GuessForm::question_selection_default_key)
    $form_state->setErrorByName(GuessForm::select_list_key, "Please select a question to guess an answer!");
  else if ($entered_answer_value == GuessForm::text_field_default_value)
    $form_state->setErrorByName(GuessForm::text_field_key, "You should remove the default guess and enter your own.");
  else
  {
    $error_message_or_success = $this->question_and_answer_validator->validateAnswerToQuestion($selected_question_key, $entered_answer_value);

    if (is_string($error_message_or_success))
      $form_state->setErrorByName(GuessForm::text_field_key, $error_message_or_success);
  }
}

Tests pass, yeah!!

The last thing we need to do, which we won’t actually show in the article (you can see it on the GitHub repository), is to remove some of the auxiliary question and answer retrieval logic from the GuessForm class. We had constants for each question type, as well as a method getAnswerToQuestion() that were on the GuessForm, that now reside on the QuestionAndAnswerValidator class. The only reason we kept them on the GuessForm until now is to keep the tests passing until we finally updated the GuessForm::validateForm method with the final modification. If you remember, we made a TODO note to remove those constants from GuessForm in the section where we created the QuestionAndAnswerKeys class.

Series Conclusion

In Working Effectively With Legacy Code (which I’ll definitely recommend again!), Michael Feathers proposes a definition for the term “legacy code.” This term can mean different things to different developers (code that smells, code you don’t want to touch, code that’s hard to change, etc), but I accept Feathers’ definition, which is to say that “legacy code” is code without unit test coverage. If you don’t have unit test coverage, you can’t “safely” change the code — as in, you can’t change code without being certain you didn’t break anything.

In the previous article, we put some legacy code under test. And in this article series, we’ve looked at just how having unit tests to cover legacy code benefits us. By having the tests in place, we could safely refactor our code to more closely model the Single Responsibility Principle. We also set ourselves up for being able to fulfill the additional requirement that was given to us in a much more modular and maintainable way.

Leave a Reply