In this post we will make the Context that will track the stats (short for statistics) for each question. This Context will be called StatsContext. StatsContext will track how many times the user has answered each question right, wrong, and how many times the user has skipped that question.
In the next post we will make a Stats component. The Stats component will show the stats to the user. The Stats component will appear on the Answering screen.
User Story
- The user sees a card. They hover their mouse over an icon and a popup appears. The popup shows the user how many times they have seen the card, and how many times they have gotten the answer right or wrong.
Features
- Stats for cards are tracked
-
Right,Wrong, andSkipbuttons updateStatsContext - User can see the stats for the card they are looking at
To make these features work we will
- Define the types for Stats
- Make the
StatsContext - Write the tests for the
StatsComponent - Make the
Statscomponent - Change the tests for
Answering - Add the
Statscomponent to Answering
Add Stats Types to Types.ts
File: src/types.ts
Will match: src/complete/types-4.ts
Add the interface Stats to types. Stats describes the stats for a single question.
//The stats for a single question
export interface Stats {
//number of times user has gotten it right
right: number,
//number of times user has gotten it wrong
wrong: number,
//number of times user has seen the question but skipped it instead of answering it
skip: number
};
Add the interface StatsType. StatsType is an object with a a string for an index signature. Putting the index signature in StatsType means that TypeScript will expect that any key that is a string will have a value that is a Stats object.
We will use the question from Cards as the key to store and retrieve the stats.
//an interface with an string index signature
//each string is expected to return an object that fits the Stats interface
//the string that we will use for a signature is the question from a Card object
export interface StatsType {
[key: string]: Stats
};
Describe the StatsDispatch function and the StatsState type.
StatsDispatch
To change the contents of StatsContext we will have our components dispatch actions to StatsContext. This works just like dispatching actions to the CardContext. To dispatch actions to StatsContext we will use useContext to get dispatch out of StatsContext inside components that use StatsContext. StatsContext contains StatsState. We have to tell TypeScript that the key 'dispatch' inside StatsState will contain a function.
StatsState
StatsState is a union type. A union type is a way to tell TypeScript that a value is going to be one of the types in the union type.
StatsState puts together StatsType and StatsDispatch. This means that TypeScript will expect a Stats object for every key that is a string in StatsState, except for 'dispatch,' where TypeScript will expect the dispatch function.
//The StatsDispatch function
interface StatsDispatch {
dispatch: (action: StatsAction) => void
};
//a union type. The stats state will have a Stats object for any given key
//except dispatch will return the StatsDispatch function
export type StatsState = StatsType & StatsDispatch
StatsActionType and StatsAction
The enum StatsActionType and the type StatsAction define the types of actions that we can dispatch to StatsContext. Later in this post you will write a case for each type of StatsAction so the reducer in StatsContext can handle it. In addition to the type, each action takes a parameter called 'question.' The 'question' is a string, same as the question from the Card objects. When the reducer receives an action, it will use the question as the key to find and store the stats.
//an enum listing the three types of StatsAction
//A user can get a question right, wrong, or skip it
export enum StatsActionType {
right = 'right',
skip = 'skip',
wrong = 'wrong'
};
//Stats Action
//takes the question from a card
export type StatsAction = {
type: StatsActionType,
question: string
};
Create StatsContext
Testing StatsContext
Our tests for StatsContext will follow the same format as the tests we wrote for CardContext. We will test the Provider, the Context, and the reducer. We will start by testing the reducer to make sure that it handles actions correctly and returns the state that we expect. We'll test that the Provider renders without crashing. Then we will write a helper component to make sure that the Context returns the right data.
Recall that the reducer is what handles actions and makes changes to the state held in a Context. The reducer will add new stats objects when it sees a question that isn't being tracked yet. The reducer will add to the stats numbers for a question when it receives an action.
Choosing What to Test
-
reducerreturns state -
reduceradds a new stats object when it receives a new question -
reducerhandles right action, returns correct stats -
reducerhandles skip action, returns correct stats -
reducerhandles wrong action, returns correct stats -
StatsContextprovides an object with Stats for questions
We'll start testing with the reducer.
Test 1: Reducer Takes State, Action and returns State
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-1.tsx
Write a comment for each test we are going to make.
//reducer
//returns state
//adds a new stats object when it receives a new question
//handles right action, returns correct stats
//handles skip action, returns correct stats
//handles wrong action, returns correct stats
//StatsContext provides an object with Stats for questions
The reducer takes a state object and an action object and returns a new state object. When the action type is undefined, the reducer should return the same state object that it received.
Imports and the first test. Declare state, an empty object. Declare action as an object with an undefined type.
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { reducer } from './index';
afterEach(cleanup);
describe('StatsContext reducer', () => {
it('returns state', () => {
const state = {};
const action = { type: undefined };
expect(reducer(state, action)).toEqual(state);
});
});
Passing Test 1: Reducer Takes State, Action and returns State
File: src/services/StatsContext/index.tsx
Will Match: src/services/StatsContext/complete/index-1.tsx
Write the first version of the reducer. Remember that the reducer takes two parameters.
The first parameter is the state object. The state object type is StatsState.
The second parameter is the action object. The action object type is StatsAction.
Imports:
import { StatsAction, StatsState } from '../../types';
Write the reducer:
//the reducer handles actions
export const reducer = (state: StatsState, action: StatsAction) => {
//switch statement looks at the action type
//if there is a case that matches the type it will run that code
//otherwise it will run the default case
switch(action.type) {
//default case returns the previous state without changing it
default:
return state
}
};
Test 2 Preparation: Add blankStats and initialState to StatsContext file
File: src/services/StatsContext/index.tsx
Will Match: src/services/StatsContext/complete/index-2.tsx
Before we write the tests, we need to add the blankStats and initialState objects to the StatsContext file.
Imports the types.
import { Stats, StatsAction, StatsState } from '../../types';
Create the blankStats object. Later, the reducer will copy this object to create the Stats object used to track new questions. Put blankStats in the file above the reducer.
//a Stats object
//use as the basis for tracking stats for a new question
export const blankStats = {
right: 0,
wrong: 0,
skip: 0
} as Stats;
Create the initialState. Put it after the reducer.
//the object that we use to make the first Context
export const initialState = {
dispatch: (action: StatsAction) => undefined
} as StatsState;
Ok, now we are ready to write the second test.
Test 2: reducer Adds a New Stats Object When it Receives a New Question
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-2.tsx
The next test we are going to write is 'adds a new stats object when it receives a new question.' That's a good thing to test. But shouldn't we test each case to make sure it works? Will we have to write three tests?
And what about all the tests after that?
- handles
rightaction, returns correct stats - handles
skipaction, returns correct stats - handles
wrongaction, returns correct stats
Those are probably going to be basically the same test. Do we really have to write the same code three times? No, we don't! Jest provides a way to make and run tests from a list of arguments. The way to make and run multiple tests from a list of arguments is the it.each method.
First we'll write a single test to show that the right case in the reducer adds a new stats object to the state. Then we'll write the code to pass that test. After that, I'll show you how to use it.each to make many tests at once when you want to test a lot of things with similar code. We'll replace the individual test with code that generates three tests, one to test each case.
Make the Single Test for reducer Handles right Action
Import the blankStats and initialState from StatsContext. Import StatsActionType from types.
import { blankStats, initialState, reducer } from './index';
import { StatsActionType } from '../../types';
Write the test.
//adds a new stats object when it receives a new question
it('adds a new stats object when it receives a new question', () => {
const question = 'Example Question';
//the action we will dispatch to the reducer
const action = {
type: StatsActionType.right,
question
};
//the stats should be the blankStats object
//with right === 1
const rightStats = {
...blankStats,
right: 1
};
//check to make sure that initialState doesn't already have a property [question]
expect(initialState[question]).toBeUndefined();
const result = reducer(initialState, action);
//after getting a new question prompt in an action type 'right'
//the question stats should be rightStats
expect(result[question]).toEqual(rightStats);
});
That looks pretty similar to the tests we've written before.

Run it, and it will fail.
Pass the Single Test for reducer Handles right Action
Now let's write the code for the reducer to handle actions with the type 'right.'
The case will need to:
Get the question out of the action.
Get the previous stats. To find the previous stats, first look in the state for a property corresponding to the question. If there are stats for the question already, use those. Otherwise, use the blankStats object.
Make the new stats. Use the previous stats, but increment the target property by one. e.g. right: prevStats.right + 1.
Make a new state object. Assign newStats as the value of the question.
Return the new state.
Remember, the cases go inside the switch statement. Add case 'right' to the switch statement in the reducer and save it.
case 'right': {
//get the question from the action
const { question } = action;
//if the question is already in state, use those for the stats
//otherwise, use blankStats object
const prevStats = state[question] ? state[question] : blankStats;
//create newStats from the prevStats
const newStats = {
...prevStats,
//right increases by 1
right: prevStats.right + 1
};
//assign newStats to question
const newState = {
...state,
[question]: newStats
};
return newState;
}
Case right, wrong and skip Will All Be Basically the Same Code
If you understand how the code for case right works, think about how you would write the code for the other cases, wrong and skip. It's pretty much the same, isn't it? You'll just be targeting different properties. wrong instead of right, etc.
What Will the Tests Look Like?
The tests will look very repetitive. In fact, the tests would be the same. To test wrong, you would copy the test for right and just replace the word 'right' with the word 'wrong.' Writing all these tests out would be a waste of time when we will have three cases that all work the same. Imagine if you had even more cases that all worked the same! Or if you wanted to test them with more than one question prompt. You would be doing a lot of copying and pasting.
Jest includes a way to generate and run multiple tests. The it.each() method.
Delete the test we just wrote for 'adds a new stats object when it receives a new question.' We don't need it anymore. We are going to replace it with code that generates and runs multiple tests.
Tests: Using it.Each to Generate Multiple Tests
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-3.tsx
it.each() is the method that generates and runs multiple tests. Because it() is an alias for test(), you can also use test.each() if you think that sounds better. We'll start out using it.each() in this post, but later in the tutorial we'll use test.each() when we run multiple tests.
The API, which means the arguments that it.each() accepts and the way you use them, are different from what you would expect. One thing to note is that the code that you write to generate the title for each test uses a weird format called printf formatting. That's why you'll see % signs in the titles when we write them.
To make it.each work we will
- Use Object.values() to get an array containing each value in the enum StatsActionType
- Use Array.map() to iterate over the StatsActionType array
- for each StatsActionType we will make an array of arguments that it.each will turn into a test
- So we'll end up with an array of arrays of test arguments
- We'll pass that array to it.each(). it.each() will print a test name based on the arguments and then run a test using the arguments
Start by making a describe block.
describe('Test each case', () => {
});
Inside the describe block 'Test each case'
Write the functions that we'll use to generate the arguments for it.each().
Make a helper function that takes a StatsActionType and returns a Stats object with the argument type set to 1.
const getStats = (type: StatsActionType) => ({...blankStats, [type]: 1});
Bracket Notation doesn't mean there's an array. Bracket notation is a way of accessing an object property using the value of the variable inside the brackets. So when you call getStats('right') you will get back an object made by spreading blankStats and setting right to 1.
The getStats returns an object. It has a Concise Body and an Implicit Return. Surrounding the return value in parentheses is a way of telling the compiler that you are returning an object. The curly brackets enclose the object that is getting returned. Without the parentheses around them, the compiler would read the curly brackets as the body of the function instead of a returned value.
Declare an example question.
const exampleQuestion = 'Is this an example question?';
Make a helper function that accepts a StatsActionType and returns a StatAction object.
//function that takes a StatsActionType and returns an action
const getAction = (
type: StatsActionType,
) => ({
type,
question: exampleQuestion
});
Inside the first describe block make another describe block. This is called 'nesting' describe blocks. Nested describe blocks will print out on the test screen inside of their parent blocks. Also, variables that are in scope for outer describe blocks will be available to inner describe blocks. So we can use all the variables we just declared in any test that is inside the outer describe block.
describe('Reducer adds a new stats object when it receives a new question prompt', () => {
});
Inside the Describe Block 'Reducer adds a new stats object when it receives a new question prompt'
Write the code to generate the arguments that we will pass to it.each.
Object.values will give us an array of each value in StatsActionType: ['right', 'skip', 'wrong'].
Array.map will iterate through each value in that array and return a new array.
In the callback function we pass to map we'll create an action object, the results that we expect to see, and return the array of arguments for the test.
//uses Array.map to take each value of the enum StatsActionType
//and return an array of arguments that it.each will run in tests
const eachTest = Object.values(StatsActionType)
.map(actionType => {
//an object of type StatAction
const action = getAction(actionType);
//an object of type Stats
const result = getStats(actionType);
//return an array of arguments that it.each will turn into a test
return [
actionType,
action,
initialState,
exampleQuestion,
result
];
});
Use it.each to run all the tests. Each test will get an array of five arguments. If we wanted to rename the arguments, we could, but to try and make it easier to read we will name the arguments the same thing that we named them when we created them.
I'm not going to explain printf syntax, but here's a link if you're curious.
//pass the array eachTest to it.each to run tests using arguments
it.each(eachTest)
//printing the title from it.each uses 'printf syntax'
('%#: %s adds new stats',
//name the arguments, same order as in the array we generated
(actionType, action, initialState, question, result) => {
//assert that question isn't already in state
expect(initialState[question]).toBeUndefined();
//assert that the stats object at key: question matches result
expect(reducer(initialState, action)[question]).toEqual(result);
});
Pass the it.each Tests for skip and wrong
File: src/services/StatsContext/index.tsx
Will Match: src/services/StatsContext/complete/index-3.tsx
Write the case for skip and add it to the switch statement. Notice that we use bracket notation and the ternary operator to get the value for prevStats.
//user skipped a card
case 'skip': {
//get the question from the action
const { question } = action;
//if the question is already in state, use those for the stats
//otherwise, use blankStats object
const prevStats = state[question] ? state[question] : blankStats;
//create newStats from the prevStats
const newStats = {
...prevStats,
//skip increases by 1
skip: prevStats.skip + 1
};
//assign newStats to question
const newState = {
...state,
[question]: newStats
};
return newState;
}
How Would You Write the Code for Case wrong?
File: src/services/StatsContext/index.tsx
Will Match: src/services/StatsContext/complete/index-3.tsx
Try writing the case to handle wrong actions on your own before you look at the example below. Hint: Look at the cases right and skip.
//user got a question wrong
case 'wrong': {
//get the question from the action
const { question } = action;
//if the question is already in state, use those for the stats
//otherwise, use blankStats object
const prevStats = state[question] ? state[question] : blankStats;
//create newStats from the prevStats
const newStats = {
...prevStats,
//wrong increases by 1
wrong: prevStats.wrong + 1
};
//assign newStats to question
const newState = {
...state,
[question]: newStats
};
return newState;
}
Test 4: Results for Existing Questions
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-4.tsx
Rewrite the helper function getStats() to take an optional parameter stats, a Stats object. The '?' tells TypeScript that the parameter is optional. If getStats receives stats, create the new Stats object by spreading the argument received for stats. Otherwise, spread the imported blankStats object.
//function that takes a StatsActionType and returns a Stats object
//may optionally take a stats object
const getStats = (
type: StatsActionType,
stats?: Stats
) => stats
? ({ ...stats,
[type]: stats[type] + 1 })
: ({ ...blankStats,
[type]: 1 });
Create a new describe block below the describe block 'Reducer adds a new stats object when it receives a new question prompt' but still nested inside the describe block 'Test each case.'
Name the new describe block 'Reducer returns correct stats.'
describe('Reducer returns correct stats', () => {
})
Inside the describe block 'Reducer returns correct stats'
Write a StatsState object, existingState.
//create a state with existing questions
const existingState = {
...initialState,
[examplePrompt]: {
right: 3,
skip: 2,
wrong: 0
},
'Would you like another example?': {
right: 2,
skip: 0,
wrong: 7
}
};
Use Object.values and Array.map to create the test arguments.
//Object.Values and array.map to turn StatsActionType into array of arrays of test arguments
const existingTests = Object.values(StatsActionType)
.map(actionType => {
//get the action with the type and the example prompt
const action = getAction(actionType);
//get the stats for examplePrompt from existingState
const stats = existingState[exampleQuestion];
//getStats gives us our expected result
const result = getStats(actionType, stats);
//return the array
return [
actionType,
action,
existingState,
result,
exampleQuestion,
];
});
Use it.each to run the array of arrays of test arguments.
it.each(existingTests)
('%#: %s returns correct stats',
(actionType, action, initialState, result, question) => {
//assert that question is already in state
expect(initialState[question]).toEqual(existingState[exampleQuestion]);
//assert that the stats object at key: question matches result
expect(reducer(initialState, action)[question]).toEqual(result);
});
That's it! Now you know one way to generate multiple tests. There are other ways to generate multiple tests. it.each() can take a template literal instead of an array of arrays. We'll make multiple tests that way later. There is also a separate library you can install and use called jest in case.
Tests That Pass When You Write Them
These tests all pass because we already wrote the code to pass them. If a test passes when you write it, you should always be at least a little suspicious that the test isn't telling you anything useful. Can you make the tests fail by changing the tested code? Try going into the index file and changing the code for one of the cases in the reducer's switch statement so it doesn't work. Does the test fail? If it still passes, then that's bad!
Test 5: StatsProvider Renders Without Crashing
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-5.tsx
Add an import of the StatsProvider from StatsContext. We will write the StatsProvider to pass this test.
import { blankStats, initialState, reducer, StatsProvider } from './index';
Make a describe block named 'StatsProvider.'
Write the test to show that the StatsProvider renders without crashing. Recall from testing CardContext that the React Context Provider component requires a prop children that is an array of components. That's why we render StatsProvider with an array of children. If you prefer, you can use JSX to put a child component in StatsProvider instead of passing the array.
//StatsContext provides an object with Stats for questions
describe('StatsProvider', () => {
it('renders without crashing', () => {
render(<StatsProvider children={[<div key='child'/>]}/>)
});
})
This test will fail because we haven't written the StatsProvider yet.
Pass Test 5: StatsProvider Renders Without Crashing
File: src/services/StatsContext/index.tsx
Will Match: src/services/StatsContext/complete/index-4.tsx
We'll use createContext and useReducer to make the StatsContext work. Import them from React.
import React, { createContext, useReducer } from 'react';
Declare the initialState. We'll put a placeholder dispatch function in there. We just have to have it to stop TypeScript from throwing an error. This placeholder makes our initialState object fit the StatsState union type that we declared. The placeholder dispatch accepts the correct type of argument, the StatsAction. But the placeholder will be replaced with the actual dispatch function inside the CardProvider.
//the object that we use to make the first Context
export const initialState = {
dispatch: (action: StatsAction) => undefined
} as StatsState;
Use createContext to create the StatsContext from the initialState.
const StatsContext = createContext(initialState);
Declare the props for the StatsProvider. StatsProvider can accept ReactNode as its children. We can also declare the optional prop testState, which is a StatsState. When we want to override the default initialState for testing purposes we just need to pass a testState prop to StatsProvider.
//the Props that the StatsProvider will accept
type StatsProviderProps = {
//You can put react components inside of the Provider component
children: React.ReactNode;
//We might want to pass a state into the StatsProvider for testing purposes
testState?: StatsState
};
Write the StatsProvider and the exports. If you want to review the parts of the Provider, take a look at the CardProvider in post 6, where we made CardContext.
We use Array Destructuring to get the state object and the dispatch function from useReducer. We return the Provider with a value prop created by spreading the state and the reducer. This is the actual reducer function, not the placeholder that we created earlier. Child components are rendered inside the Provider. All child components of the Provider will be able to use useContext to access the StatsContext.
const StatsProvider = ({ children, testState }: StatsProviderProps) => {
const [state, dispatch] = useReducer(reducer, testState ? testState : initialState);
const value = {...state, dispatch} as StatsState;
return (
<StatsContext.Provider value={value}>
{children}
</StatsContext.Provider>
)};
export {
StatsContext,
StatsProvider
};
Great! Now the StatsProvider renders without crashing.
Test 6: Does Stats Context Provide Stats Values
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-6.tsx
To test if the StatsProvider is providing the correct values for StatsContext, we are going to write a helper component. Let's list the features we are trying to test:
Features
- provides value for right
- provides value for skip
- provides value for wrong
Import useContext from React.
import React, { useContext} from 'react';
Inside the 'StatsProvider' describe block, make the helper component StatsConsumer. StatsConsumer uses useContext to access StatsContext, and will display the stats that it receives. Rendering StatsConsumer will allow us to check if StatsContext and StatsProvider are working correctly.
//A helper component to get Stats out of StatsContext
//and display them so we can test
const StatsConsumer = () => {
const stats = useContext(StatsContext);
//stats is the whole StatsState
//one of its keys is the dispatch key,
//so if there's only 1 key there's no stats
if (Object.keys(stats).length < 2) return <div>No Stats</div>;
//use the filter method to grab the first question
const question = Object.keys(stats).filter(key => key !== 'dispatch')[0];
const { right, skip, wrong } = stats[question];
//display each property in a div
return <div>
<div data-testid='question'>{question}</div>
<div data-testid='right'>{right}</div>
<div data-testid='skip'>{skip}</div>
<div data-testid='wrong'>{wrong}</div>
</div>
};
Create exampleQuestion and testState. You can copy and paste the existingState from inside the 'reducer' describe block above.
const exampleQuestion = 'Is this an example question?';
//create a state with existing questions
const testState: StatsState = {
...initialState,
[exampleQuestion]: {
right: 3,
skip: 2,
wrong: 0
},
'Would you like another example?': {
right: 2,
skip: 0,
wrong: 7
}
};
Make a nested describe block 'StatsContext provides stats object.' Make a helper function renderConsumer to render StatsConsumer inside the StatsProvider. Pass StatsProvider the testState object.
Test question, right, skip, and wrong.
//StatsContext returns a stats object
describe('StatsContext provides stats object', () => {
const renderConsumer = () => render(
<StatsProvider testState={testState}>
<StatsConsumer/>
</StatsProvider>)
it('StatsConsumer sees correct question', () => {
const { getByTestId } = renderConsumer();
const question = getByTestId('question');
expect(question).toHaveTextContent(exampleQuestion);
})
it('StatsConsumer sees correct value of right', () => {
const { getByTestId } = renderConsumer();
const right = getByTestId('right');
expect(right).toHaveTextContent(testState[exampleQuestion].right.toString());
})
it('StatsConsumer sees correct value of skip', () => {
const { getByTestId } = renderConsumer();
const skip = getByTestId('skip');
expect(skip).toHaveTextContent(testState[exampleQuestion].skip.toString());
})
it('StatsConsumer sees correct value of wrong', () => {
const { getByTestId } = renderConsumer();
const wrong = getByTestId('wrong');
expect(wrong).toHaveTextContent(testState[exampleQuestion].wrong.toString());
})
})
Test 7: it.each() With Tagged Literal
File: src/services/StatsContext/index.test.tsx
Will Match: src/services/StatsContext/complete/test-7.tsx
it.each() can take an array of arrays. it.each can also accept a tagged literal. A tagged literal, or template literal, sounds way more complicated than it is. A tagged literal is information inside of backticks. They are pretty common in modern javascript, and very useful.
To use a tagged literal for your it.each tests, you basically write out a table and let it.each run through the table. You declare the names of your arguments in the top row, and separate everything with the pipe | character.
Delete the three tests that we wrote for the value of right, skip, and wrong. Replace them with this example of it.each using a tagged literal.
This example also calls it by its alternate name, test. Remember, the 'it' method is an alias for the 'test' method. So calling test.each is the same as calling it.each. I think "test each" sounds better than "it each," so I usually use test.each when I'm running multiple tests.
it('StatsConsumer sees correct question', () => {
const { getByTestId } = renderConsumer();
const question = getByTestId('question');
expect(question).toHaveTextContent(exampleQuestion);
});
test.each`
type | expected
${'right'} | ${testState[exampleQuestion].right.toString()}
${'skip'} | ${testState[exampleQuestion].skip.toString()}
${'wrong'} | ${testState[exampleQuestion].wrong.toString()}
`('StatsConsumer sees correct value of $type, returns $expected',
({type, expected}) => {
const { getByTestId } = renderConsumer();
const result = getByTestId(type);
expect(result).toHaveTextContent(expected);
});
See how in the top row we named our arguments? The first column is named 'type' and the second column is named 'expected.' Also notice that when we are printing the title we can refer to them by name instead of using the printf format. Like I said earlier, the test.each API is different from how you'd expect it to be.
We use object destructuring to get type and expected out of the arguments passed to each test. Then writing the tests goes as normal.
If you have a few minutes, try adding another column to the arguments. Try renaming the arguments. Try changing the titles of the tests, and rewriting the matchers and assertions.
Ok, now we have confidence that the StatsProvider is working. Let's import the StatsProvider into the App, then make the Stats component that will show Stats to the user.
Import StatsProvider into the App
File: src/App.tsx
Will Match: src/complete/app-4.tsx
We've got the StatsContext written. Now let's make the stats from StatsContext available to the components. You will make StatsContext available by importing the StatsProvider into the App and wrapping the components in the StatsProvider.
Go to /src/App.tsx. Change it to this:
import React from 'react';
import './App.css';
import Answering from './scenes/Answering';
import { CardProvider } from './services/CardContext';
import { StatsProvider } from './services/StatsContext';
const App: React.FC = () =>
<CardProvider>
<StatsProvider>
<Answering />
</StatsProvider>
</CardProvider>
export default App;
Great! Now the contents of the stats context will be available to the Answering component. It will also be available to any other components that you put inside the StatsProvider.
Try Refactoring
Look at the code for the StatsContext reducer. Cases right, skip, and wrong have almost the same code inside of them. They each get the previous stats the same way. They each create the nextStats object and the nextState object the same way.
Can you write a single function getPrevStats that each case can call to get the previous stats for a question? Hint: You can pass the state to a function just like any other object. You'll know if your function works or doesn't because the tests will tell you if you break anything.
Can you write a single function getNextStats that each case can call that will return the next stats value?
If you write these functions and replace all the code inside the cases with them, you're eliminating duplicate code without changing the way the code works. That is called refactoring, and it's a big part of Test Driven Development.
Next Post
In the next post we will make the Stats Component that will show the stats to the user.











Top comments (0)