The 5 principles of Unit Testing
Tags: testing, webdev, javascript, programming
Published: 2023-09-14 14:14:43

Get your software development to the next level with these 5 powerful principles of unit testing! Testing your code is one of the most important aspects of software development, as it ensures quality, scalability, and reliability of your product.  

But, without any guidelines, it can be difficult to write effective tests. In fact, testing code can become more complex and harder to maintain than the actual production code! 

Don't let this happen to you. Follow these best practices for unit testing, which can be applied to any framework or library, both in the backend or frontend of your application, and you'll be able to write lean, accurate, and easy-to-read testing code. With these, you'll save valuable dev time, avoid technical debt, and enjoy testing your code: 
<br/>

##1. Lean and accurate testing 

Testing code must be **simple and easy to work with**. Anyone looking at the test should know instantly what the test is about and what its purpose is. Developing tests should bring great value with very little effort and time investment.  


> _Do you need more than 30 seconds to read and understand your test? Rewrite it!_ 

Testing just for ‘coverage percentage’? Don’t!!! This just makes the majority of the test suite to be unnecessary and undesirable for the developers. Test **only what’s needed**. It’s better to drop some tests for agility and simplicity, **testing only the main business logic and principal edge cases**.   
<br/> 
##2. Test the behaviour, not the implementation 

Don’t check every single line and internal variable changed in the code. When testing, you should **focus on the result**. The result should always remain the same even if the code inside the method is refactored! 

This way, you **won’t need to rewrite your tests if the code base is changed**. 

```javascript
// Wrong ❌ - Test behaviour
describe('Evaluation Service', () => {
  describe('Register Students', () => {
    it('Should add new students to the evaluation service', () => {
      const studentJosh = {
        id: 1,
        name: 'Josh McLovin',
        average: 6.98,
      }
      evaluationService.addStudent(studentJosh)
        
      expect(evaluationService._students[0].name).toBe('Josh')
      expect(evaluationService._students[0].average).toBe(6.98)
    })
  })
})
```

```javascript
// Right ✅ - Test behaviour
describe('Evaluation Service', () => {
  describe('Register Students', () => {
    it('Should add new students to the evaluation service', () => {
      const studentJosh = {
        id: 1,
        name: 'Josh McLovin',
        average: 6.98,
      }

      evaluationService.addStudent(studentJosh)
      expect(evaluationService.getStudentAverage('Josh')).toBe(6.98)
    })
  })
})
```
<p>&nbsp;</p>

##3. Test naming and structuring. AAA pattern.  

Have you ever had a failing test named ‘_it should [...] correctly_’ and lost a couple of minutes finding where the problem was? 

Naming and structuring your test suite can enhance your ability to address any failing tests swiftly and accurately, ultimately saving you valuable time. So, let's dive into two key principles to keep in mind for your next testing endeavours:  
 

### 3.1 Thoughtful test naming:  

&emsp;When naming your tests, attempt to incorporate the following information: 

  &emsp; - **What** is being tested? 
  &emsp; - Under **what circumstances**? 
  &emsp; - What is the **expected outcome**? 

```javascript
// Right ✅ - Test naming

// 1. What is being tested:
describe('Evaluation Service', () => {
  describe('Evaluate Students', () => {
    // 2 & 3. Context and expected result
    it('If the student grade is below the minimum grade, student should be suspended', () => {
      const students = [
        { name: 'Mark', grade: 4.25 },
        { name: 'Colin', grade: 6.7 },
        { name: 'Ben', grade: 5.3 },
      ]

      const result = evaluationService.evaluateStudents({ students, minGrade: 5 })

      expect(result['Mark']).toBe('suspended')
    })
  })
})
```
<p>&nbsp;</p>

### 3.2 The AAA pattern for testing code structure:  

&emsp;If you want to maintain a readable and easily comprehensible test suite, structure  the test as it follows:  

- **Arrange**: set up all the code needed to simulate the required situation. This can include initialising variables, mocking responses, instantiating the unit under test, etc. 
- **Act**: execute what is being tested, usually in a single line of code.
- **Assert**: check the obtained result is the expected one. Like the one above, this should take only one line.

```javascript
// Right - AAA Testing Pattern
describe('Evaluation Service', () => {
  describe('Average Calculation', () => {
    it('Should calculate the average grade of all the students', () => {
      // Arrange: create an object with the student names and their grades
      const students = [
        { name: 'Mark', grade: 4 },
        { name: 'Colin', grade: 10 },
        { name: 'Ben', grade: 7 },
        { name: 'Tim', grade: 3 },
      ]
  
      // Act: execute the getAverage method
      const avg = evaluationService.getAverage(students)
  
      // Assert: check if the result is the expected one -> (4+10+7+3)/4 = 6
      expect(avg).toEqual(6)
    })
  })
})
```
<p>&nbsp;</p>


##4. Deterministic and isolated tests 


If a single failing test turns your entire suite red, then you may not be approaching it in the right way! 
 
Tests should be **independent and isolated**, targeting and dealing with one specific logic at a time, accomplishing a faster and more stable test suite. 

What happens **if you don’t** write your tests independently?

- You won’t be able to pinpoint the **exact cause and location** of the bugs and issues.  
- When refactoring the tests, you will have to **update and synchronise multiple ones**.  
- You won’t be able to run your tests in any order, which may cause **breaking or skipping some of the assertions or expectations**.
<p>&nbsp;</p>

##5. Property-based testing and realistic data 

Tired of writing a large amount of possible inputs in your tests? Property-based testing does it for you! But… What 's that? 


Property-based testing creates hundreds of possible combinations, stressing the test and **increasing the opportunity of uncovering previously unnoticed bugs**. This approach can even return the inputs which may have caused an unexpected outcome. 

Libraries like [JSVerify](https://github.com/jsverify/jsverify) or [Fast-Check](https://github.com/dubzzz/fast-check) offer essential tools to facilitate property-based testing.

However, if you prefer not to dig into such extensive testing, it is crucial to utilize **realistic data** whenever possible. Inputs like 'abc' or '1234' might erroneously pass the test when it should actually fail. 

```javascript
// Wrong ❌ - False Positive - Test that passes even though it shouldn't 
class EvaluationService {
  _students =  [];

  addStudent(student) {
    // Add the student if the name has no numbers
    if(!student.name.matches(/^([^0-9]*)$/)){
      this._students.push(student);
    }
  }
}

describe('Evaluation Service', () => {
  describe('Register Students', () => {
    it('Should add students to the Evaluation service', () => {
      const mockStudent = {
        id: 2,
        name: 'username',
        average: 7
      }
      // Won't fail because the name is a string without number -> We are not checking what happens if the user
      // inputs a name with a number.
      evaluationService.addStudent(mockStudent) 
      expect(evaluationService.getStudentAverage('username')).toBe(7)
    })
  })
})
// In the example above, we are making a test so the test passes, instead of looking for edge cases with realistic data

```
<p>&nbsp;</p>

##Bonus Tip! 

If you are getting a hard time testing any logic in your component, this could reflect that, maybe, you should break your component logic into smaller, easier and testable pieces of code!  
<br/>

In conclusion, following this best practices may lead to a new, readable and maintainable way of testing your beloved production code. Thanks for reading! 

 
> A tested app is a reliable app!