Home/Blog/Programming/Testing in software development: A practical guide
Home/Blog/Programming/Testing in software development: A practical guide

Testing in software development: A practical guide

Aasim Ali
May 12, 2023
14 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

Software testing for a developer involves creating and running tests to identify and isolate defects, bugs, or issues in the code, as well as ensure that the software is reliable, robust, and scalable. By performing various types of testing, developers can improve the overall quality and reliability of the software, and ultimately deliver a better product to their users.

Jest is the testing framework for JavaScript. It is good for any JavaScript framework, be it Vue, Angular, React, Meteor.js, Express, jQuery, Laravel, Bootstrap, Node.js, or others. It applies to any kind of testing—client and server, unit and snapshot, and internal and external dependency. This blog is great for software engineers looking to upgrade their skills so they can build better applications in terms of stability and scalability, and it will focus on unit, integration, and snapshot tests only.

We’ll cover:

Welcome to the practical guide to using Jest—a JavaScript testing framework that focuses on simplicity. Quality tests allow for confident and quick iteration, while poorly written tests can provide a false sense of security.

Jest is a test runner that executes tests seamlessly across environments and JavaScript libraries. This blog aims to teach Jest in a framework-agnostic context so that it can be used with different JavaScript frameworks. Jest provides an environment for documenting, organizing, and making assertions in tests while supporting the framework-specific logic that will be tested.

React App Testing

Cover
React App Testing

This path is a deep dive into testing in React. First, you will learn test-driven development in React that will serve as basic for advanced concepts further explained in this path. Then, this path will teach unit testing and integration testing using tools such as Jest and Selenium. Finally, this path will explain end-to-end testing of React apps in detail.

33hrs 30mins
Beginner
61 Playgrounds
13 Quizzes

In Jest, matchers are functions that are used to test if a value meets certain criteria. Other than testing the expected flow, Jest also allows testing for error and exceptions. This is an important part of writing high-quality software. Apart from the various matchers available in Jest, there are additional matchers that extend its functionality that are provided by external libraries.

Test Automation

Cover
Test Automation

Test Automation allows us to validate and automatically review software products like web applications so we can check their quality standards. Test Automation is rapidly evolving around the globe because of its efficient auto-reviewing and validating process. This path will teach you to design a test automation framework for automating web-based and mobile applications. Moreover, it will help you automate performance testing using Gatling, Puppeteer, and Lighthouse tools. By the end of this path, you’ll be able to design your own test automation frameworks for web and mobile applications.

14hrs
Beginner
11 Playgrounds
10 Quizzes

A test checks if Y is true or false given X. The X can be an argument, API response, state, or user behavior, while Y can be a return value, error, or presence in the DOM. To test, use expect(value) and a matcher like .toEqual(expectedValue). The evaluation of the value in a test is called an assertion and it’s done using matchers, which are a set of functions accessible via expect. For example: expect(getSum(1,2)).toEqual(3); is a way of writing the test in Jest. There are various types of matchers in Jest.

Types of tests#

Jest supports several types of tests, but this blog will focus only on the following:

  1. Unit tests: Jest allows unit tests to be written that verify the functionality of small, individual parts of your code, such as functions and methods.
  2. Integration tests: Jest also supports integration tests, which verify the interactions between different parts of the code, such as multiple functions or modules.
  3. Snapshot tests: With Jest, you can easily create snapshot tests, which are used to test the visual output of your code, such as HTML or CSS.

Overall, Jest provides a comprehensive testing solution for JavaScript developers, with support for a wide range of testing types and easy integration with other tools and frameworks.

Although unit tests are the most basic type of test, they play a critical role in ensuring the stability of a system. Unit tests are essential for creating a dependable and resilient testing suite since they are straightforward to write and determine what to test. Additionally, writing unit tests helps to enforce best practices in code.

Unit tests are designed to test individual units of code, such as a single function, conditional statement, or low-level display component. Focusing on one thing at a time ensures that the code behaves as expected, including edge cases and error handling. Unit tests ensure that a small block of code functions well on its own.

Testing React Apps with Jest and React Testing Library

Cover
Testing React Apps with Jest and React Testing Library

The skills acquired from this course will allow you to write automated tests on your React app using Jest and React Testing Library, which are the de-facto testing tools for React. These tests will allow you to ship new versions of your app with confidence. The tests will also allow you to increase your release frequency, allowing you to get features to your customers quicker. You will learn how to write robust tests that are resilient to breaking when code is improved and refactored, reducing maintenance costs. The course will teach you how to simulate a user interacting with the app. Beyond that, it will also teach you how to robustly test asynchronous parts of the app. Regular quizzes and exercises will reinforce knowledge throughout the course.

16hrs
Intermediate
89 Playgrounds
7 Quizzes

Unit test examples#

The single responsibility principle states that a function should do only one thing, making testing easier. By separating concerns and creating clean interfaces, test maintenance can be limited to the functions directly related to the changed code. This is important for overall code maintenance and scalability, but it also helps with testing, allowing the focus to remain on one section and one test suite when making changes to the code base. Here are three simple example codes for unit tests in Jest.

1. Testing a function that adds two numbers

function addNumbers(a, b) {
  return a + b;
}

test('adds two numbers', () => {
  expect(addNumbers(2, 3)).toBe(5);
});

In this example,

  • A function addNumbers is defined, which takes two numbers as inputs and returns their sum.
  • A test that uses the expect method is written to verify that addNumbers(2, 3) returns 5.

2. Testing a function that converts Fahrenheit to Celsius

function convertFtoC(fahrenheit) {
  return (fahrenheit - 32) * 5/9;
}

test('converts Fahrenheit to Celsius', () => {
  expect(convertFtoC(32)).toBe(0);
  expect(convertFtoC(68)).toBe(20);
  expect(convertFtoC(100)).toBeCloseTo(37.7778);
});

In this example,

  • A function convertFtoC is defined, which takes a temperature in Fahrenheit as input and returns the temperature in Celsius.
  • Three tests that use the expect method are written to verify that convertFtoC returns the correct output for three different inputs.

3. Testing a function that filters an array

function filterArray(arr, condition) {
  return arr.filter(condition);
}

test('filters an array', () => {
  const input = [1, 2, 3, 4, 5];
  const expectedOutput = [2, 4];
  const isEven = (num) => num % 2 === 0;
  expect(filterArray(input, isEven)).toEqual(expectedOutput);
});

In this example,

  • A function filterArray is defined, which takes an array and a condition function as inputs and returns a new array that only contains elements that satisfy the condition.
  • A test is written that creates an input array [1, 2, 3, 4, 5], an expected output array [2, 4], and a condition function that filters for even numbers.
  • The expect method is used to verify that filterArray(input, isEven) returns expectedOutput.

Code is complex, and testing large sequences of code can be difficult to do well. Unit tests allow developers to concentrate on one thing and test it thoroughly. Once this is done successfully, that block of code can be used elsewhere with confidence because it has been tested thoroughly. Breaking up code into smaller, independent units allows those units to be strung together in more complex ways through a few additional tests.

Writing tests can be challenging and time-consuming, but certain best practices in code can make writing tests more accessible, less tedious, and less time intensive. The DRY principle, which stands for ‘don’t repeat yourself,’ encourages the abstraction of common code into a unique, singular function, making code more maintainable and saving time. When it comes to testing, this principle allows logic to be tested only once, rather than for each context in which it is used.

While unit tests are designed to test individual units of code, integration tests verify that different parts of the system work well together. Integration testing is a critical part of software testing since it can uncover issues that may not be apparent when testing individual units of code.

Integration testing typically involves testing the interaction between multiple components, such as different modules or services within an application. For example, an integration test might ensure that data is correctly passed between a front-end component and a back-end API. Integration tests can also verify that third-party services, such as payment gateways or authentication providers, are correctly integrated with the system.

Complete Guide to Testing React Apps with Jest and Selenium

Cover
Complete Guide to Testing React Apps with Jest and Selenium

This course will teach you to properly test your React applications, starting from basic unit tests with Jest to automated integration tests using Selenium. You will learn everything from the ground up to the most advanced concepts that will give you confidence in shipping your software to customers.

13hrs
Beginner
18 Playgrounds
3 Assessments

Integration test examples#

Integration tests can help uncover issues such as miscommunication between different components, incorrect data formatting or transfer, and compatibility problems between different parts of the system. These issues can be challenging to uncover with unit tests alone, making integration testing a critical part of a comprehensive testing strategy. Here are three example codes for integration testing in Jest.

1. Testing a simple Express route

const request = require('supertest');
const app = require('../app');

describe('GET /hello', () => {
  it('responds with "Hello, world!"', async () => {
    const response = await request(app).get('/hello');
    expect(response.statusCode).toBe(200);
    expect(response.text).toBe('Hello, world!');
  });
});

In this example,

  • The supertest library makes an HTTP request to the Express app and verifies that the response has the expected status code and body text.

2. Testing an API endpoint with supertest

const request = require('supertest');
const app = require('../app');

describe('GET /api/books', () => {
  it('responds with a list of books', async () => {
    const res = await request(app).get('/api/books');
    expect(res.statusCode).toBe(200);
    expect(res.body).toHaveLength(3);
    expect(res.body[0].title).toBe('Book 1');
  });
});

In this example,

  • An HTTP GET request is sent to an API endpoint /api/books implemented in app.js by using the supertest.
  • The response status code (200) is checked, the response body is an array of three books, and the first book has the title Book 1.

3. Testing a database connection with knex

const knex = require('knex');
const config = require('../knexfile');
const { createTable, insertData, getData } = require('../db');

describe('database connection', () => {
  let db;

  beforeAll(async () => {
    db = knex(config);
    await createTable(db);
    await insertData(db);
  });

  afterAll(async () => {
    await db.destroy();
  });

  it('retrieves data from the "books" table', async () => {
    const books = await getData(db, 'books');
    expect(books).toHaveLength(3);
    expect(books[0].title).toBe('Book 1');
  });
});

In this example,

  • A set of functions that create a table, insert data, and retrieve data are tested using knex, which connects to the database.
  • The getData function is checked to return an array of three books, and the first book has the title Book 1.

While integration testing can be more complex than unit testing, there are several best practices that can make it easier to write effective integration tests. One approach is to use mocks or stubs to simulate the behavior of external services or components that are not available during testing. This approach can help isolate the system being tested and reduce the complexity of integration testing.

Another best practice is to use a test environment that closely mirrors the production environment. This can help ensure that the system is tested under realistic conditions and that any issues uncovered during testing are likely to occur in production. Additionally, it is essential to automate integration tests as much as possible to ensure that they run regularly and consistently.

Snapshot tests are another type of test that Jest supports. They capture a snapshot of the output of a component or function and compare it to a previously saved version. This type of test ensures that the output of a component or function remains consistent over time and prevents unexpected changes.

Snapshot tests are useful for UI components because they ensure that changes to the interface are intentional and predictable. When a snapshot test fails, it means that something has changed in the UI that was not expected and that it’s necessary to investigate the change and determine whether it is intentional or not.

Testing Vue.js Components with Jest

Cover
Testing Vue.js Components with Jest

Front-end frameworks, in general, are exploding in popularity, and Vue.js, in particular, has become one of the most popular ones. As front-end frameworks continue to grow in usage, the ability to incorporate testing has become an essential feature. It's simply the only scalable way to ensure your app is functioning as expected. You can learn to test your Vue.js applications thoroughly using Jest with this interactive course. You'll start by learning the simplest unit tests in Jest, and will gradually build up to more intricate tests of different Vue components. To give you practical front-end testing experience, you'll be running your tests on a predefined data rendering Vue.js application. This course is essential for anyone looking to create robust and effective front-end tests for their Vue apps.

7hrs
Advanced
27 Playgrounds
9 Quizzes

Snapshot test examples#

To write a snapshot test, Jest takes a snapshot of the component or function’s output and saves it as a file. The next time the test runs, Jest compares the current output to the saved snapshot. If the output has changed, Jest highlights the difference and prompts the developer to either update the snapshot or investigate the change. Here are three examples of snapshot tests in Jest.

1. An example code for a button click

import React from 'react';
import renderer from 'react-test-renderer';
import MyButton from './MyButton';

describe('MyButton', () => {
  test('onClickHandler renders correctly', () => {
    const handleClick = jest.fn();
    const tree = renderer.create(<MyButton onClick={handleClick} />).toJSON();
    expect(tree).toMatchSnapshot();
    tree.props.onClick();
    expect(handleClick).toHaveBeenCalledTimes(1);
    expect(tree).toMatchSnapshot();
  });
});

In this example,

  • Whether the onClickHandler function of MyButton component works as expected is checked.
  • A mock function called handleClick is first created using jest.fn(), which will be passed to the component as the onClick prop.
  • Then the component is rendered with renderer.create(), and a snapshot of the initial tree is stored using toJSON().
  • The onClick event is triggered by calling tree.props.onClick(), and whether handleClick was called once using toHaveBeenCalledTimes(1) is checked.
  • Finally, whether the resulting tree matches a previously stored snapshot using toMatchSnapshot() is checked.

2. An example code that selects an item from a dropdown list

import React from 'react';
import renderer from 'react-test-renderer';
import MyDropdown from './MyDropdown';

describe('MyDropdown', () => {
  test('selecting an item from dropdown renders correctly', () => {
    const handleChange = jest.fn();
    const tree = renderer.create(<MyDropdown onChange={handleChange} />).toJSON();
    expect(tree).toMatchSnapshot();

    const select = tree.children[0];
    select.props.onChange({ target: { value: 'item2' } });
    expect(handleChange).toHaveBeenCalledTimes(1);
    expect(handleChange).toHaveBeenCalledWith('item2');

    expect(tree).toMatchSnapshot();
  });
});

In this example,

  • Whether selecting an item from the MyDropdown component works as expected is checked.
  • A mock function called handleChange using jest.fn() is first created, which will be passed to the component as the onChange prop.
  • Then renderer.create() is used to render the component and store a snapshot of the initial tree using toJSON().
  • An item is selected from the dropdown by simulating a change event with a target.value of item2.
  • Whether handleChange was called once with item2 using toHaveBeenCalledTimes(1) and toHaveBeenCalledWith('item2') is checked.
  • Finally, whether the resulting tree matches a previously stored snapshot using toMatchSnapshot() is checked.

3. An example code for editing a text field

import React from 'react';
import renderer from 'react-test-renderer';
import MyTextField from './MyTextField';

describe('MyTextField', () => {
  test('editing text field renders correctly', () => {
    const handleChange = jest.fn();
    const tree = renderer.create(<MyTextField onChange={handleChange} />).toJSON();
    expect(tree).toMatchSnapshot();

    const input = tree.children[0];
    input.props.onChange({ target: { value: 'updated text' } });
    expect(handleChange).toHaveBeenCalledTimes(1);
    expect(handleChange).toHaveBeenCalledWith('updated text');

    expect(tree).toMatchSnapshot();
  });
});

In this example,

  • Whether editing a text field in the MyTextField component works as expected is checked.
  • A mock function called handleChange is created using jest.fn(), which will be passed to the component as the onChange prop.
  • The component is rendered with renderer.create(), and a snapshot of the initial tree is stored using toJSON().
  • Next, an edit to the text field is simulated by triggering a change event with a target.value of updated text.
  • Whether handleChange was called once with updated text using toHaveBeenCalledTimes(1) and toHaveBeenCalledWith('updated text') is checked.
  • Finally, whether the resulting tree matches a previously stored snapshot using toMatchSnapshot() is checked.

Snapshot tests are helpful when designed and maintained properly. However, Jest cannot distinguish between intentional and unintentional changes in our user interface. Client-side code can be placed into two buckets—direct UI changes and functionality changes. Direct UI changes require updating snapshots, while functionality changes may or may not require updates. If a bug in the functionality affects what the user sees, snapshots may need updating. If changes fundamentally impact the end result for the user, snapshots will need updating.

Snapshot tests can also be helpful when refactoring code or when making small changes to an application. They provide a way to ensure that changes don’t inadvertently affect other parts of the system. While snapshot tests are not a replacement for unit tests, they complement them and help to create a robust and dependable testing suite.

Wrapping up and next steps#

Jest provides a powerful set of tools for developers to create and maintain unit, integration, and snapshot tests. These tests enable developers to identify issues early in the development process, ensure that changes to one part of the system do not break the other parts, and monitor changes to the user interface. By leveraging Jest for testing, software development teams can improve the overall quality of their code, increase efficiency, and reduce the risk of introducing new bugs and issues.

Educative currently offers the following career-specific paths for developers:

The following specific courses on software testing are also offered by Educative:

Happy learning!


  

Free Resources