BDD, TDD with Jest, React and React Hooks
Photo by Bich Tran from Pexels

BDD, TDD with Jest, React and React Hooks

Both BDD and TDD refer to the methods of software development. Behavior-driven development (BDD) uses natural language statements to build a common understanding of the application. 

Whenever we start TDD on of any application feature we need to answer a few questions:

  1. What are we testing?
  2. Where to store the tests?
  3. Suite names?
  4. Test case names?
  5. What are we asserting?

Decide what you're building / testing with simple requirements:

Let's begin with the sample feature requirement.  As part of a bigger application, a client wants us to build a product management feature:

  1. List Products
  2. Add Product
  3. Update an existing Product
  4. Delete an existing Product

Wireframe of a product management feature

What are we testing

To take us nearer to a starting point, we have to turn these requirements into features using a BDD approach.

We are going to use the BDD template for each scenario:

GIVEN (some starting condition)
WHEN (some action occurs)
THEN (expect some result)
Product List
  1. GIVEN that a user is on the Product List Screen, WHEN the page loads, THEN product list data must be retrieved and displayed showing the name and price of each product in the list.
  2. GIVEN that a user is on the Product List Screen WHEN the user clicks on an Add Product button THEN the user must be taken to the New Product Screen.
New Product
  1. GIVEN that a user is on the New Product Screen WHEN the captures the name and price of a new product and clicks on a Save button THEN the new product data must be saved.
Product Detail
  1. GIVEN that a user is on the Product Detail Screen WHEN the screen loads THEN the existing product data must be retrieved.
  2. GIVEN that a user is on the Product Detail Screen WHEN the edits the name and price of an existing product and clicks on a Save button THEN the updated product data must be saved.
  3. GIVEN that a user is on the Product Detail Screen WHEN the Delete button is clicked THEN the product data must be deleted.

flow

Let's get started

Let's get started with the Product List View Model. This is where all logic (the brain) for the Product List View will be kept. Note: We'll be building this using React and React Hooks. If you want to learn about custom hooks, read more here

What do we name our test suites?

A test suite is a collection of one or multiple test cases. In this case the test suite is "Product List View Model"

What do we name our test cases?

Test cases contain the details and steps that one would need to perform to verify a certain behaviour. So typically will begin with "should...."

Where do we store our Tests?

When writing tests I prefer to store my tests in a folder structure which mirrors the application. The view model is under test, so we create a test suite for this view model in the test folder.

Test Suite: Product List View Model

├─ src
    ├─ Domain
    │   └─ UseCase
    │       └─ Product
    │           └─ GetProducts.js
    └─ Presentation
        └─ View
            └─ Product
                ├─ List
                │   ├─ Components
                │   │   ├─ ProductList.js
                │   │   └─ AddButton.js
                │   ├─ View.js
                │   └─ ViewModel.js
                └─ index.js

└─ tests
    Presentation
        └─ View
            └─ Product
                └─ List
                    └─ ViewModel.test.js
1/** 2 * @jest-environment jsdom 3 */ 4import useProductListViewModel from '../../../../../src/Presentation/Views/Product/List/ViewModel'; 5import { renderHook, act } from '@testing-library/react-hooks' 6import "babel-polyfill" 7 8describe("Product List View Model", () => { 9 10 it('should return empty product list', () => { 11 //GIVEN 12 const { result } = renderHook(() => useProductListViewModel()) 13 14 //WHEN 15 16 //THEN 17 expect(result.current.products).toEqual([]) 18 }); 19}) 20

fail_to_compile

This test fails because we haven't created the view model. Let's create the bare minimum to make the test pass.

1import { useState } from "react" 2 3export default function ProductListViewModel() { 4 5 const [products, setProducts] = useState([]); 6 7 return { 8 products 9 } 10}

fail_to_compile

Let's create another test to check for products once getProducts is called

1 it('should return 2 items after get products use case', async () => { 2 //GIVEN 3 const { result } = renderHook(() => useProductListViewModel()) 4 const expectedResult = [{ name: "Product One", price: 99 }, { name: "Product Two", price: 99 }] 5 6 //WHEN 7 await act(async () => result.current.getProducts()) 8 9 //THEN 10 expect(result.current.products).toBe(expectedResult) 11 });

fail_to_compile

Let's modify our view model to make the test pass.

1import { useState } from "react" 2import getProductsUseCase from '../../../../Domain/UseCase/Product/GetProducts' 3 4export default function ProductListViewModel() { 5 6 const [products, setProducts] = useState([]); 7 8 async function getProducts() { 9 const { result, error } = await getProductsUseCase.invoke(); 10 setProducts(result) 11 } 12 return { 13 getProducts, 14 products, 15 } 16} 17

Let's also mock the getProductsUseCase in our test. Note we don't need a concrete implementation of this use case to test our view model. This can be an empty file for now.

1... 2import mockGetProductsUseCase from '../../../../../src/Domain/UseCase/Product/GetProducts' 3... 4 it('should return expected result after getProducts is called', async () => { 5 //GIVEN 6 const { result } = renderHook(() => useProductListViewModel()) 7 const expectedResult = [{ name: "Product One", price: 99 }, { name: "Product Two", price: 99 }] 8 mockGetProductsUseCase.invoke = jest.fn().mockImplementation(() => Promise.resolve({ result: expectedResult, error: "" })); 9 10 //WHEN 11 await act(async () => result.current.getProducts()) 12 13 //THEN 14 expect(result.current.products).toBe(expectedResult) 15 }); 16

fail_to_compile

And so, we can continue using this way of testing by adding test suites and test cases for each view model or module we'd like to test.

Conclusion:

Using a BDD flavour of TDD allows us to write more focused code and get feedback on the quality of the application design.