Test Driven Development | Explained with Examples and Tools
This blog talks about test driven development as well as its importance. Moreover, it also elaborates test driven development with examples.
What is test driven development (TDD) ?
Test driven development is the process of writing tests before writing the actual code. It is a method in software development in which you first write a test and it fails, then write the code to pass the test, and then clean up the code. This method ensures that the code is always tested and reduces bugs, improving code quality and correctness.
Test driven development examples
Let's consider an ecommerce application feature - adding an item to a shopping cart for test driven development.
Step 1: Write a Failing Test
Test: Adding an Item to the Cart (Using Rspecs)
# spec/cart_spec.rb
require 'cart'
RSpec.describe Cart do
it 'adds an item to the cart' do
cart = Cart.new
item = { id: 1, name: 'Laptop', price: 1000 }
cart.add_item(item)
expect(cart.items).to include(item)
end
end
Step 2: Write the Minimum Code to Pass the Test
Sample cart class
# lib/cart.rb
class Cart
attr_reader :items
def initialize
@items = []
end
def add_item(item)
@items << item
end
end
Step 4: Extend the Test
Next we move on to writing additional tests to handle more complex cases like calculating the total price of the items in the cart.
Sample Test: Calculating Total Price
RSpec.describe Cart do
it 'calculates the total price of items in the cart' do
cart = Cart.new
item1 = { id: 1, name: 'Laptop', price: 1000 }
item2 = { id: 2, name: 'Mouse', price: 50 }
cart.add_item(item1)
cart.add_item(item2)
expect(cart.total_price).to eq(1050)
end
end
Step 4: Implement the Code to Pass the New Test
class Cart
attr_reader :items
def initialize
@items = []
end
def add_item(item)
@items << item
end
def total_price
@items.sum { |item| item[:price] }
end
end
Step 5: Refactor
Cart class :
# lib/cart.rb
class Cart
attr_reader :items
def initialize
@items = []
end
def add_item(item)
@items << item
end
def total_price
@items.sum { |item| item[:price] }
end
end
Test:
# spec/cart_spec.rb
require 'cart'
RSpec.describe Cart do
it 'adds an item to the cart' do
cart = Cart.new
item = { id: 1, name: 'Laptop', price: 1000 }
cart.add_item(item)
expect(cart.items).to include(item)
end
it 'calculates the total price of items in the cart' do
cart = Cart.new
item1 = { id: 1, name: 'Laptop', price: 1000 }
item2 = { id: 2, name: 'Mouse', price: 50 }
cart.add_item(item1)
cart.add_item(item2)
expect(cart.total_price).to eq(1050)
end
end
Summary
In this TDD process, we started by writing a failing test, then wrote the minimum code to pass the test, and finally refactored the code to improve it. This cycle repeats as you add more features and functionality, ensuring that the codebase remains clean and maintainable.
TDD Vs. Traditional Testing
Here are some key differences between TDD and traditional testing:
Test Driven Development | Traditional Testing |
---|---|
Tests are written before the feature code is written. | Tests are written after the complete feature code has been implemented |
Emphasises on writing small tests that focus on a specific functionality leading to incremental development and continuous feedback. | Tests are written to cover a wide range of functionalities. The focus is on ensuring the code implemented meets the requirements |
Helps in identifying bugs early in the development process. | Bugs are identified later in the development process, leading to more time spent on debugging and fixing issues |
Code is written to pass the tests, leading to a more modular and maintainable codebase. | Code is written first and then tested, leading to a less modular and maintainable codebase. |
Encourages developers to think about the design and architecture of the code before writing it. | Developers focus on writing code that meets the requirements without considering the design and architecture. |
Refactoring is an integral part of the development process, leading to cleaner and more efficient code. | Refactoring is done after the code has been implemented, leading to more time spent on cleaning up the code. |
5 steps of test driven development
There are 5 steps in the TDD flow, which includes the TDD cycle of Red, Green and Refactor.
Step 1: Understand the requirements
Before getting started with the tests or the code implementation it is important to understand the requirements and the expected behaviour of the feature. This forms a strong foundation for the tests and the code that will be written. Example - Adding an item to the shopping cart.
Step 2: Add a test, which will fail at this point (Red)
The first step in the TDD cycle involves translating the requirement to a test case. The test is written to validate the functionality that needs to be implemented. Since the code is yet to be written, the test will definitely fail during the first execution. Example - Writing a test to add an item to the cart.
Step 3: Write minimum code required to pass the failing test (Green)
At this point, you know the requirement and you have a failing test. The next step is to write the minimum code required to pass the test. The code should be written in a way that it passes the test and nothing more. Example - Writing the code to add an item to the cart.
Step 4: Refactor the code
Once the test passes, it's important to refactor the code. Refactoring the code ensures that the code is clean, maintainable and efficient.
Step 5: Repeat the cycle
With TDD the emphasis is on writing small tests that focus on a specific functionality leading to incremental development and continuous feedback. The first test that is written focuses on specific features (like adding an item to the cart). Now the next task is to focus on another feature (like calculating the total price of the items in the cart) and repeat the cycle.
Frameworks for Test Driven Development
Several frameworks support TDD across different programming languages. Below is a list of popular TDD frameworks:
Ruby: RSpec, Minitest
Rspec is a poular testing framework for Ruby that supports BDD (Behaviour Driven Development) and TDD. RSpec’s syntax is very close to natural language, making it easy to read and understand.
Example of an Rspec test
RSpec.describe Calculator do
it 'adds two numbers' do
calculator = Calculator.new
result = calculator.add(2, 3)
expect(result).to eq(5)
end
end
Minitest is another testing framework for Ruby that is lightweight and fast. It is a good choice for developers who prefer a minimalistic testing framework.
Example of a Minitest test
require 'minitest/autorun'
class TestCalculator < Minitest::Test
def test_adds_two_numbers
calculator = Calculator.new
result = calculator.add(2, 3)
assert_equal 5, result
end
end
JavaScript: Jasmine, Mocha, Jest
Jasmine testing framework support both BDD and TDD. It is easy to set up and use, and provides a clean syntax for writing tests. It requires no additional dependencies or configurations.
Example of a Jasmine test
describe('Calculator', function() {
it('adds two numbers', function() {
var calculator = new Calculator();
var result = calculator.add(2, 3);
expect(result).toBe(5);
});
});
Mocha is a flexible testing framework that supports both BDD and TDD. It is widely used in the JavaScript community and provides a rich set of features for writing tests. It allows developers to choose their preferred assertion libraries, mocking tools, and reporters.
Example of a Mocha test
const assert = require('assert');
const Calculator = require('./calculator');
describe('Calculator', function() {
it('adds two numbers', function() {
const calculator = new Calculator();
const result = calculator.add(2, 3);
assert.strictEqual(result, 5);
});
});
Jest is a popular testing framework developed by Facebook known for it's zero config set-up, support for snapshot testing, mocking, and code coverage. Jest is the go-to choice for many developers working with React applications.
Example of a Jest test
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Calculator from './Calculator'; // Adjust the path as needed
describe('Calculator Component', () => {
it('adds two numbers and displays the result', () => {
const { getByText } = render(<Calculator />);
const button = getByText('Add 2 and 3');
fireEvent.click(button);
const resultText = getByText('Result: 5');
expect(resultText).toBeInTheDocument();
});
});
Python: PyTest, unittest
PyTest is a versatile testing framework for Python known for its simplicity and flexibility. Powerful features like fixtures and parameterized testing, and easy integration with other tools make it a popular choice for developers.
Example of a PyTest test
import pytest
from calculator import Calculator # Adjust the path as needed
def test_adds_two_numbers():
calculator = Calculator()
result = calculator.add(2, 3)
assert result == 5
unittest is the built-in testing framework for Python that is part of the standard library. It offers extensive support for test organization, setup/teardown methods, and test suites.
Java: JUnit, TestNG
JUnit is a popular testing framework for Java that supports TDD. It provides annotations for writing tests, assertions for validating results, and test runners for executing tests. It integrates seamlessly with build tools like Maven and Gradle.
Example of a JUnit test
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class CalculatorTest {
@Test
public void testAddsTwoNumbers() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
}
TestNG is another testing framework for Java that provides additional features like parallel testing, data-driven testing, and flexible test configuration. It's well-suited for complex, large-scale testing scenarios in enterprise applications.
Example of a TestNG test
import org.testng.Assert;
import org.testng.annotations.Test;
public class CalculatorTest {
@Test
public void testAddsTwoNumbers() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
Assert.assertEquals(result, 5);
}
}
C#: NUnit, MSTest
NUnit is a popular, open source testing framework for C#. It supports advanced features like parameterized tests and is well-integrated with various CI/CD tools.
Example of a NUnit test
using NUnit.Framework;
[TestFixture]
public class CalculatorTests
{
[Test]
public void AddsTwoNumbers()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
}
MSTest is Microsoft's built-in testing framework for .NET, providing seamless integration with Visual Studio and Azure DevOps.
Example of a MSTest test
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void AddsTwoNumbers()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
}
PHP: PHPUnit
PHPUnit is the de facto testing framework for PHP, offering a powerful and easy-to-use environment for unit testing. It supports a wide range of assertions, mock objects, and code coverage analysis, making it ideal for ensuring code quality and reliability in PHP applications.
<?php
use PHPUnit\Framework\TestCase;
class CalculatorTests extends TestCase
{
public function testAddsTwoNumbers()
{
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
}
C++: Catch2
Catch2 is a modern, header-only C++ testing framework known for its simplicity and ease of use. It provides an expressive and intuitive syntax, with minimal boilerplate, making it easy to write and maintain tests. Catch2 supports a wide range of features, including BDD-style testing, test case discovery, and rich output formatting.
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
#include "Calculator.h"
TEST_CASE("Calculator adds two numbers", "[Calculator]") {
Calculator calculator;
int result = calculator.add(2, 3);
REQUIRE(result == 5);
}
About DevAssure
DevAssure is an end-to-end test automation platform for QA champs. With the help of DevAssure, users can perform Web testing, Visual Testing, API testing, Accessibility testing with a few clicks.
DevAssure's desktop application is supported in Windows, MacOS, Linux. To know more about how devassure can leverage test automation capabilities, please click the below button to schedule a personalized demo.