
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:
- What are we testing?
- Where to store the tests?
- Suite names?
- Test case names?
- 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:
- List Products
- Add Product
- Update an existing Product
- Delete an existing Product
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
- 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.
- 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
- 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
- GIVEN that a user is on the Product Detail Screen WHEN the screen loads THEN the existing product data must be retrieved.
- 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.
- GIVEN that a user is on the Product Detail Screen WHEN the Delete button is clicked THEN the product data must be deleted.
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
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}
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 });
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
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.