Clean Code: Unit Tests

Himanshu Ganglani
4 min readJul 7, 2023

--

Unit testing is an essential practice in software development that involves testing individual units of code to ensure they function as expected. Writing clean and effective unit tests not only helps to identify bugs and issues early on but also improves the maintainability and reliability of the codebase. Writing clean and effective unit tests not only helps to identify bugs and issues early on but also improves the maintainability and reliability of the codebase.

The Three Laws of TDD

Test-Driven Development (TDD) follows three fundamental laws, which guide the development process:

  • First Law: You must write a failing test before writing any production code. This ensures that the test accurately reflects the desired behavior of the code being written.
  • Second Law: You must write only enough production code to make the failing test pass. This encourages incremental development and prevents unnecessary code complexity.
  • Third Law: You must write a failing test for each new requirement before writing the production code to fulfill that requirement. This reinforces the TDD cycle and ensures that new functionality is adequately tested.

Keeping Tests Clean

Just as clean code is essential for production code, clean tests are crucial for unit tests. Clean tests are readable, maintainable, and focused on a single responsibility. Key principles for keeping tests clean include:

  • Readability: Tests should be easy to read and understand, acting as documentation for both current and future developers.
  • Expressiveness: Tests should clearly convey the intent of the code being tested, using descriptive names and well-structured assertions.
  • Maintainability: Tests should be written with maintainability in mind, allowing for easy modifications and updates as the code evolves.
  • Minimize Test Data: Tests should use minimal test data to focus on the essential behavior being tested.

Test-Driven Development (TDD)

Test-Driven Development (TDD) is a software development approach that follows a specific cycle: Red-Green-Refactor. The steps involved in TDD are:

  1. Red: Write a failing test that reflects the desired behavior of the code being developed.
  2. Green: Write the minimum amount of code required to make the failing test pass.
  3. Refactor: Improve the code without changing its behavior, ensuring it remains clean, readable, and maintainable.

TDD encourages a test-first approach, where tests are written before the production code. It helps drive the design of the code, ensures test coverage, and provides confidence in the code’s correctness.

Testing Boundary Conditions

Boundary conditions represent the edges of input ranges or special cases that can lead to bugs. It is essential to test these conditions to ensure the code behaves correctly. Some examples of boundary conditions include:

  • Empty or null inputs
  • Minimum and maximum values
  • Lower and upper bounds of loops and arrays
  • Negative values
  • Boundary conditions related to performance or scalability

Testing boundary conditions helps uncover issues that may arise due to edge cases and ensures the code handles them appropriately.

The Test Double Strategy

Test doubles are objects or functions that replace dependencies of the code being tested. They simulate the behavior of the real dependencies and allow for isolated unit testing. The different types of test doubles include:

  • Dummy: A simple placeholder used to satisfy method parameters but is never actually used.
  • Stub: Provides predefined responses to method calls, returning fixed values.
  • Mock: Similar to a stub, but with additional verification capabilities to check if certain methods were called.
  • Fake: A simplified implementation of a dependency that behaves similarly to the real implementation but with simplified logic or reduced complexity.
  • Spy: Records information about method invocations, such as the number of times a method was called or the arguments passed.

Using test doubles helps isolate the code under test and control the behavior of dependencies during unit testing.

F.I.R.S.T.

Clean tests follow five other rules that form the above acronym
Fast: Unit tests should execute quickly, enabling frequent execution during development and integration processes.

Independent: Each unit test should be independent of others, meaning they can be executed in any order and in isolation.

Repeatable: Unit tests should produce the same result regardless of when or where they are executed.

Self-Validating: Unit tests should have a boolean output, indicating whether they pass or fail without any manual interpretation.

Timely: Unit tests should be written before the code they are testing, promoting a test-driven development (TDD) approach.

Mocking and Test Doubles

Mocking is a technique used to create and control test doubles. Mocking frameworks or libraries provide functionality to create and manage mocks. In JavaScript, popular mocking libraries include Jest, Sinon.js, and Testdouble.js.

Let’s consider an example using Sinon.js for mocking in JavaScript

// Test - userService.test.js
const sinon = require('sinon');
const emailService = require('./emailService');
const userService = require('./userService');

describe('registerUser', () => {
it('should send a welcome email', () => {
// Arrange
const sendEmailStub = sinon.stub(emailService, 'sendEmail');
const username = 'john';
const email = 'john@example.com';

// Act
userService.registerUser(username, email);

// Assert
sinon.assert.calledWith(sendEmailStub, email, `Welcome,john!`);

// Restore the stubbed method
sendEmailStub.restore();
});
});

In this example, we use Sinon.js to create a stub for the sendEmail method of the emailService module. We stub the method to simulate its behavior without actually sending an email. Then, we call the registerUser function and assert that the sendEmail method was called with the expected arguments using Sinon's assert.calledWith method. Finally, we restore the stubbed method using restore() to ensure it does not affect other tests.

Conclusion

Unit testing is a vital aspect of software development that promotes code quality, bug detection, and maintainability. By following the principles of unit testing, adhering to TDD practices, and understanding the concepts of test doubles and boundary conditions, developers can create robust and reliable code. Incorporating unit tests into the development process improves code quality, facilitates collaboration, and helps deliver high-quality software systems.

--

--

Himanshu Ganglani

Software Engineer with 5 year of experience in Javascript, Node.Js, AWS, typescript, Docker.