Mocking the service: Learning Drupal 8 unit test

Have been working on big Symfony project, PHP unit test has become my friend and it really helped me to organize my code in a better structure. Since Drupal 8 has implemented phpunit in core, I was excited to try phpunit in Drupal 8 in a little test module of mine.

The difference between functional test and unit test is: 

  • Functional test consider your function is a black box, and you have no idea what's happening inside. All you need to test is what output should you get according to your input. Functional test always requires to bootstrap the whole application to test.
  • Whereas unit test only aims to test a single unit without bootstrap the whole application, you should know if the unit is working exactly the way you designed it to. 

Here comes the example:

In this example, I have a service that replaces terms in the text with links to their pages. I.E. if I have a taxonomy term called Drupal, and this service will replace all the 'Drupal' text in the content to

<a href="/link/to/drupal/term/page">Drupal</a>

. You would know how can I do this, just define a filter plugin, and do the actual replacement in the function process. To remove the logic from the plugin, I created a service to handle with the logic and inject the service to the filter plugin. The function would be like below:

public function termfilterPerformSubs($text, $list) {
    
some preparing...

      // For each word, check if it matches our term filter.
      foreach ($words as $key => $word) {
        if (!empty($word)) {
          if (isset($fast_array[$word])) {
            $term = taxonomy_term_load_multiple_by_name($word, $vid);
            $words[$key] = Link::fromTextAndUrl($word, Url::fromRoute(
              'entity.taxonomy_term.canonical', 
              ['taxonomy_term' => $id]
            ))
            ->toString();
          }
        }
      }
      return implode('', $words);
  
some clean up ...
  
}

In the loop, we check by word to see if the word is in selected vocabulary, then find out the link and replace it. This function is the core of the module, and we'd better to write unit test to make sure it is working as we expected. 

We should follow the namespace and structure standard to create our test class. In the class, we setup fixtures of the term list and example text we want to filter. The we can do assertion:

  /**
   * Test the replacement function.
   */
  public function testPerformSubs() {
    $this->assertSame(
      $this->TermfilterReplacement->termfilterPerformSubs(
        $this->getTestText(), 
        $this->getTestTermList()
      ), 
      '<a href="LINK/TO/TERM">foo</a> bar');
  }

  /**
   * Get mocked text data.
   *
   * @return string
   *   Mocked text.
   */
  protected function getTestText() {
    return 'foo bar';
  }

  /**
   * Get mocked List data.
   *
   * @return array
   *   Mocked list.
   */
  protected function getTestTermList() {
    return [
      'foo' => 'tags',
    ];
  }

Then we meet the problem. We are using the Drupal function to get the term ID and find the link of a term, which means we will have to bootstrap Drupal to create the sample data and run the test. Luckily, we can mock any class we want in phpunit, then have the mocked class function to return our sample data. Wait, it still doesn't solve our problem, does it? Even we can mock our service, we still can't go around the Drupal functions! 'Wrap them in separate functions!' is the answer:

/**
   * Wrapper function to return term object by term name and vocabulary ID.
   *
   * @param $word
   *   Term name.
   * @param $vid
   *   Vocabulary ID.
   *
   * @return array
   *   Array of term objects.
   */
  public function getTermByName($word, $vid) {
    return taxonomy_term_load_multiple_by_name($word, $vid);
  }

  /**
   * Wrapper function to return term link by term ID.
   *
   * @param $id
   *   Term ID.
   *
   * @param $word
   *   Link Text.
   *
   * @return String
   *   Term URL.
   */
  public function getUrlByTermId($id, $word) {
    $link = Link::fromTextAndUrl($word, Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $id]));

    return $link->toString();
  }

And we can call them in our function: 

// For each word, check if it matches our term filter.
      foreach ($words as $key => $word) {
        if (!empty($word)) {
          if (isset($fast_array[$word])) {
            $term = $this->getTermByName($word, $fast_array[$word]);
            $words[$key] = $this->getUrlByTermId(array_keys($term)[0], $word);
          }
        }
      }

In this way, we can mock our service and have the two Drupal function wrapper to return whatever we want:

  /**
   * TermfilterReplacement object.
   */
  protected $TermfilterReplacement;

  /**
   * {@inheritdoc}
   */
  public function setUp() {
    $this->TermfilterReplacement = $this->getMock(
      '\Drupal\termfilter\TermfilterReplacement', 
      array('getTermByName', 'getUrlByTermId'));

    $this->TermfilterReplacement->expects($this->any())->method('getTermByName')
      ->willReturn([
        '7' => [
          (object) ['name' => 'foo', 'tid' => 7],
        ],
      ]);

    $this->TermfilterReplacement->expects($this->any())->method('getUrlByTermId')
      ->willReturn('<a href="/term/7">foo</a>');

  }

To sum up, if we want to unit test a service function, we need to make sure putting Drupal functions in separate wrapper functions. Then we can mock our service and make the wrapper functions return our sample data. 

If you want to test in PHPStorm, here is the setting steps.

Tags