StatsContext
jacobwicks
Posted on January 17, 2020
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
, andSkip
buttons 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
Stats
Component - Make the
Stats
component - Change the tests for
Answering
- Add the
Stats
component 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
-
reducer
returns state -
reducer
adds a new stats object when it receives a new question -
reducer
handles right action, returns correct stats -
reducer
handles skip action, returns correct stats -
reducer
handles wrong action, returns correct stats -
StatsContext
provides 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
right
action, returns correct stats - handles
skip
action, returns correct stats - handles
wrong
action, 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.
Posted on January 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.