# Unit Testing Best Practises

### **Characteristics of a Good Unit Test** <a href="#user-content-characteristics-of-a-good-unit-test" id="user-content-characteristics-of-a-good-unit-test"></a>

**Self-checking** - a test should automatically determine if it has passed or failed without user interaction

**Repeatable** - a test should return consistent results provided all input values remain the same between test runs

**Fast** - tests should not require much time to execute

**Timely** - unit tests should not require a disproportionate amount of time to write compared to the code that they are testing. If tests are taking a long time to write compared to the code being tested, consider refactoring the code to be more testable.

**Isolated** - unit tests should not depend on outside factors and be run in isolation

### Code Coverage <a href="#user-content-code-coverage" id="user-content-code-coverage"></a>

A high code coverage cannot determine the quality of the code, but is simply a metric of the amount of code being tested. Not all methods need to be tested (eg: accessor methods), but it is important to maintain as high of a code coverage metric as possible on TESTABLE code to help maintain the stability of the software as it grows and is adapted.

### Best Practises <a href="#user-content-best-practises" id="user-content-best-practises"></a>

Try to avoid introducing dependencies on infrastructure when writing unit tests. These make tests slow and possibly unreliable and should be reserved to integration tests.

#### Naming Conventions <a href="#user-content-naming-conventions" id="user-content-naming-conventions"></a>

The name of a test should consist of 2-3 parts:

1. the name of the method being tested
2. the condition under which the test occurs (optional)
3. the expected behaviour under the condition invoked

Naming standards are important as they verbosely express the intent of a test.

Example of proper naming conventions:

```
public void createBasicEvent_returnsNewEvent() 
```

```
public void createBasicEvent_invalidTitle_shouldFailValidation()
```

Example of improper naming convention:

```
public void basicTest()
```

```
public void createEvent()
```

#### Arrangement Within a Test <a href="#user-content-arrangement-within-a-test" id="user-content-arrangement-within-a-test"></a>

**Arrange, Act, Assert** is a common pattern when unit testing. As the name implies, it consists of three main actions:

* **Arrange** your objects, creating and setting them up as necessary.
* **Act** on an object.
* **Assert** that something is as expected.

Example of improper test design:

```
public void add_emptyvalues_shouldReturn0() {
   //arrange
   $calculator = new Calculator();

   //assert
   Assert.equals(0, $calculator->add('',''));
}
```

Example of proper test design:

```
public void add_emptyvalues_shouldReturn0() {
   //arrange
   $calculator = new Calculator();

   //act
   $result = $calculator->add('','');

   //assert
   Assert.equals(0, $result);
}
```

Readability is one of the more important aspects when writing good unit tests. Clearly separating what is being tested from the **arrange** and **assert** steps allows for multiple assertions to be performed on a single result.

Separating each of these actions within the test clearly highlight the dependencies required to call your code, how your code is being called, and what you are trying to assert. While it may be possible to combine some steps and reduce the size of your test, the primary goal is to make the test as readable as possible.

#### Avoid Literal Strings <a href="#user-content-avoid-literal-strings" id="user-content-avoid-literal-strings"></a>

Naming of variables in unit tests is just as important as naming conventions in code (if not more so). Unit tests should never contain literal strings. This removes the need for the reader of the test to inspect the code to determine what makes the value important.

example of string literal (no indication of reasoning):

```
public void createEvent_shouldSetColor() {
   //arrange
   $event = new Event();
   //act
   $event->bgColor = '#BD0D1B'; //there is no obvious reason for this choice
   //assert
   ...
}
```

For Codeception an example of proper reason indication would be:

```
    /**
     * @param  FunctionalTester  $I
     *
     * @return void
     * @throws Exception
     */
    public function testBackgroundColorBlack(FunctionalTester $I) {
        $this->changeBGColor($I, self::BACKGROUND_BLACK);
        $this->createEvent($I);
        $I->amOnPage('/admin/events');
        $I->clickTextOf(self::REMOVE_FILTERS);
        $I->waitForText(self::TITLE);
        $I->clickTextOf(self::CALENDAR_VIEW);
        $I->waitForText(self::TITLE);

        $I->seeInSource('background-color:' . self::BACKGROUND_BLACK . ';color:white;">' . self::TITLE);
    }
```

#### Avoid Logic in Tests <a href="#user-content-avoid-logic-in-tests" id="user-content-avoid-logic-in-tests"></a>

When writing your unit tests avoid manual string concatenation and logical conditions such as `if`, `while`, `for`, `switch`, etc.

This reduces the chance to introduce a bug inside of your tests. It also focuses more on the result rather than the implementation details.

```
public void add_excessUsers_throwsException() {
   //arrange
   $service = new UserService()

   //act
   for($index = 0; $index < 100; $index++) { //iterative logic should be avoided
      try {
          $service->addUser(new User());
      }catch(\ExcessUsersException $) {

      }
   }
   //assert
   ...
}
```

When you introduce logic into your test suite, the chance of introducing a bug into it increases dramatically. The last place that you want to find a bug is within your test suite. You should have a high level of confidence that your tests work, otherwise, you will not trust them. Tests that you do not trust, do not provide any value. When a test fails, you want to have a sense that something is actually wrong with your code and that it cannot be ignored.

`Tip: If logic in your test is unavoidable consider splitting the test up into 2 or more separate tests`

#### Avoid Multiple 'Acts' Inside of a Test <a href="#user-content-avoid-multiple-acts-inside-of-a-test" id="user-content-avoid-multiple-acts-inside-of-a-test"></a>

Each test should perform only one 'act' in a test. That means a separate test should be written for each Act.

**Why?**

* If a test fails it may not be clear which act is failing
* It ensures each test is focused on a single condition
* Ensures the name of the test matches the Act

Example of test with multiple acts:

```
public void add_emptyvalues_shouldReturn0() {
   //arrange
   $calculator = new Calculator();

   //act
   $result1 = $calculator->add('','');
   $result2 = $calculator->add('0','');
   $result3 = $calculator->add('0','0');

   //assert
   Assert.equals(0, $result1);
   Assert.equals(0, $result2);
   Assert.equals(0, $result3);
}
```

Example of tests properly segregated acts:

```
public void add_emptyvalues_shouldReturn0() {
   //arrange
   $calculator = new Calculator();

   //act
   $result = $calculator->add('','');

   //assert
   Assert.equals(0, $result);
}

public void add_single0_shouldReturn0() {
   //arrange
   $calculator = new Calculator();

   //act
   $result = $calculator->add('0','');

   //assert
   Assert.equals(0, $result);
}

public void add_double0s_shouldReturn0() {
   //arrange
   $calculator = new Calculator();

   //act
   $result = $calculator->add('0','0');

   //assert
   Assert.equals(0, $result);
}
```

#### Avoid Validating Private Methods <a href="#user-content-avoid-validating-private-methods" id="user-content-avoid-validating-private-methods"></a>

There should be no need to test a private method. Private methods are an implementation detail. At some point in the code there will be a public method that calls the private method as part of its process. What matters is the end result from the public facing method. If you feel the desire to test a private method you are leaning towards debugging, which is not the intent of a unit test.

### Code Coverage <a href="#user-content-code-coverage-1" id="user-content-code-coverage-1"></a>

All aspects of a method should be properly covered with unit tests. That means every `if/else`, `try/catch`, `switch/case`, and `return` should have a corresponding test provided. This is part of the process of 'proving' the code works as expected.

example:

```
class MixedSalad {
    const DETERMINED_VEGETABLE_TOMATO = "sorry kid, it's a tomato for dinner";
    const DETERMINED_VEGETABLE_CARROT = "carrots are said to be good for the eyes";
    const DETERMINED_VEGETABLE_GRAPE = "grapes are not a vegetable";
    const DETERMINED_VEGETABLE_UNEXPECTED = "I've no idea what you sent me";

    const ALLOWED_VEGETABLE_TOMATO = 'tomato';
    const ALLOWED_VEGETABLE_CARROT = 'carrot';
    const ALLOWED_VEGETABLE_GRAPE = 'grape';

    public function determineVegetable(string $something) : string {
        switch ($something) {
           case self::ALLOWED_VEGETABLE_TOMATO:
              return self::DETERMINED_VEGETABLE_TOMATO;
           case self::ALLOWED_VEGETABLE_CARROT:
              return self::DETERMINED_VEGETABLE_CARROT;
           case self::ALLOWED_VEGETABLE_GRAPE:
              return self::DETERMINED_VEGETABLE_GRAPE;
           default:
              return self::DETERMINED_VEGETABLE_UNEXPECTED;
        }
    }
}
----- tests ----

public function testTomato_shouldReturnFound() {
    //arrange
   $class = new MixedSalad();

   //act
   $result = $class->determineVegetable(MixedSalad::ALLOWED_VEGETABLE_TOMATO);
  
   //assert
   $this->assertEquals(MixedSalad::DETERMINED_VEGETABLE_TOMATO);
}

public function testCarrot_shouldReturnFound() {
    //arrange
   $class = new MixedSalad();

   //act
   $result = $class->determineVegetable(MixedSalad::ALLOWED_VEGETABLE_CARROT);
  
   //assert
   $this->assertEquals(MixedSalad::DETERMINED_VEGETABLE_CARROT);
}

public function testGrape_shouldReturnFound() {
    //arrange
   $class = new MixedSalad();

   //act
   $result = $class->determineVegetable(MixedSalad::ALLOWED_VEGETABLE_GRAPE);
  
   //assert
   $this->assertEquals(MixedSalad::DETERMINED_VEGETABLE_GRAPE);
}

public function testUnexpected_shouldReturnNotFound() {
    //arrange
   $class = new MixedSalad();

   //act
   $result = $class->determineVegetable('');
  
   //assert
   $this->assertEquals(MixedSalad::DETERMINED_VEGETABLE_UNEXPECTED);
}
```

Every case in the above example has a test to ensure maximum coverage.

If we neglected to write a test for 'grape' the code coverage map would indicate the omission.

### File Locations <a href="#user-content-file-locations" id="user-content-file-locations"></a>

For Unit and Integration tests (API repository) it is advisable to follow the namespace path of the file being tested.

```
/tests
  /Integration
    /Courses
      /Http
        /Controllers
          CurriculumCourseControllerTest.php
```

### File Naming Conventions <a href="#user-content-file-naming-conventions" id="user-content-file-naming-conventions"></a>

File names should be verbose in identifying which class they are testing.

For Unit tests it is recommended that files match the name of the class plus the word 'Test'.

example:

`CurriculumCourseControllerTest` will test the CurriculumCourseController.

`LotteriesServiceTest` will test the LotteriesService.

### Establishing a Baseline <a href="#user-content-establishing-a-baseline" id="user-content-establishing-a-baseline"></a>

It is recommended to prove the method works as intended under normal operating conditions. The first tests on the page is often the 'baseline' test to show that a class's methods work as intended, followed by the 'challenge' tests that are aimed at proving the code functions as intended under abnormal conditions.

### Data Providers

Data providers are used when the same test is executed with different data inputs and expected results. This is useful when you are testing a method that has multiple if/else or switch/case statements.

```
public function determineVegetable(string $something) : string {
        switch ($something) {
           case self::ALLOWED_VEGETABLE_TOMATO:
              return self::DETERMINED_VEGETABLE_TOMATO;
           case self::ALLOWED_VEGETABLE_CARROT:
              return self::DETERMINED_VEGETABLE_CARROT;
           case self::ALLOWED_VEGETABLE_GRAPE:
              return self::DETERMINED_VEGETABLE_GRAPE;
           default:
              return self::DETERMINED_VEGETABLE_UNEXPECTED;
        }
    }
```

Instead of duplicating the test method and changing the inputs, a Data Provider can be used:

```
private $classToTest;

public void setUp()
{
   //arrange
   $this->classToTest = new MixedSalad();
}

/**
* @dataProvider provideVegetablesData
*/
public function testDetermineVegetable(string $expected, string $testData): void
{
   //act
   $result = $this->classToTest->determineVegetable($testData);
  
   //assert
   $this->assertEquals($expected, $result);
}
```

```
public function provideVegetablesData()
{
    return [
        'vegetable is tomato' => [
            VegetablesService::ALLOWED_VEGETABLE_TOMATO,
            VegetablesService::DETERMINED_VEGETABLE_CARROT,
        ],
        'vegetable is lettuce' => [
            VegetablesService::ALLOWED_VEGETABLE_CARROT,
            VegetablesService::DETERMINED_VEGETABLE_CARROT,
        ],
        'vegetable is grape' => [
            VegetablesService::ALLOWED_VEGETABLE_GRAPE,
            VegetablesService::DETERMINED_VEGETABLE_CARROT,
        ],
        'vegetable is undetermined' => [
            'unkown item',
            VegetablesService::DETERMINED_VEGETABLE_UNEXPECTED,
        ],
    ];
}
```

[More information on Data Providers](https://blog.martinhujer.cz/how-to-use-data-providers-in-phpunit/)&#x20;

### TL;DR <a href="#user-content-tldr" id="user-content-tldr"></a>

Readability is key

Avoid literal strings in favour of `const` values

A test method name should consist of 2-3 parts

A test method should do 3 things: Arrange, Act, Assert

Only 1 Act per test

Avoid 'logic' in tests (loops, etc)

Every logic branch `if/else`, `switch/case`, `try/catch` should have a corresponding test

File paths should reflect the path of the class being tested

Filenames should reflect the name of the class

Use a Data Provider when repeating tests on a function with varied arguments passed


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.elentra.org/technical/developers/troubleshooting/testing-with-codeception/unit-testing/unit-testing-best-practises.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
