Skip to content

Instantly share code, notes, and snippets.

@sailesh97
Last active January 10, 2023 13:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sailesh97/b80e927e04ae1e43e93f22d8df9e4708 to your computer and use it in GitHub Desktop.
Save sailesh97/b80e927e04ae1e43e93f22d8df9e4708 to your computer and use it in GitHub Desktop.
Lecture - 5: Project Setup
Lecture - 6 - Running Tests
npm run test
Lecture - 7 - Anatomy of a Test
test
test(name, fn, timeout):
The first arguement is the test name used to identify the test
The second arguement is a function that contains the expectations to test
The 3rd arguement is timeout which is an optional arguemnt for specifying how long to wait before aborting the test. The default timeout value is 5
seconds.
expect and test are functions of jest library provided to us globally by create-react-app setup.
Lecture - 8 - Your first test
Greet.test.jest
----
```
import {render, screen} from '@testing-library/react'
import { Greet } from './greet/greet'
test('Greet renders correctly', () => {
render(<Greet/>)
const textEl = screen.getByText("hello");
expect(textEl).toBeInTheDocument();
})
```
Lecture - 9 - Test Driven Development
Test Driven Development
red-green refctoring
Lectture - 10 - JEST Watch mode
npm test script we run runs jest in watch mode behind the scenes by default
Jest Watch Mode:
Watch mode is an option that we can pass to Jest asking to watch files that have changed since last commit and execute tests related only to those changed
files.
An optimization designed to make your tests run fast regardless of how many tests you have
Note:
We have two test files till this lecture; i.e. App.test.js & greet.test.js
Let say we commited the changes and in vs code's source control file there is no file left to be committed.
Now you change something in greet.test.js only.
Now if you run npm test: In o/p screen it will log 1 test suite which refers to the changed file.
Lecture - 11 - Filtering Tests
test.only : to run a specific test out of all tests present in a file
test.skip : to skip a specific test out of all tests present in a file
---
test.only('Greet renders with a name', () => {
render(<Greet name="Sailesh"/>)
const textEl = screen.getByText("Hello Sailesh");
expect(textEl).toBeInTheDocument()
})
test.skip('Greet renders with a name', () => {
render(<Greet name="Sailesh"/>)
const textEl = screen.getByText("Hello Sailesh");
expect(textEl).toBeInTheDocument()
})
Lecture - 12 - Grouping Tests
describe(name, fn)
The first arguement is the group name
The second arguement is a function that contains the expectaions to test
Example:
describe("Greet ", () => {
test('renders correctly', () => {
render(<Greet />)
const textEl = screen.getByText("Hello");
expect(textEl).toBeInTheDocument()
})
describe.skip('Nested', () => {
test('renders with a name', () => {
render(<Greet name="Sailesh"/>)
const textEl = screen.getByText("Hello Sailesh");
expect(textEl).toBeInTheDocument()
})
})
})
Note:
1. We can use .only() and .skip() in describe as well
describe.only("name", () => {})
2. We can have nested describe blocks as well.
3. We can have multiple describe blocks in a single file.
Special Note:
1. Everytime we run npm test in command line, it logs the output in cmd.
In output, number of test suites refers to number of *.test.js files executed and not the number of describe blocks.
Lectture - 13 - Filename Conventions
- Files with .test.js or .test.tsx suffix
- Files with .spec.js or .spec.tsx suffix
- Files with .js or .tsx suffix in __tests__ folders
Recommendation is to always put your tests next to the code they are testing so that relative imports are shorter.
Note:
With JEST, we can use test() or as an alternative to test we can it() as well.
test.only() is equivalent to fit()
test.skip() is equivalent to xit()
Example:
describe("Greet ", () => {
fit('renders correctly', () => {
render(<Greet />)
const textEl = screen.getByText("Hello");
expect(textEl).toBeInTheDocument()
})
describe.skip('Nested', () => {
xit('renders with a name', () => {
render(<Greet name="Sailesh"/>)
const textEl = screen.getByText("Hello Sailesh");
expect(textEl).toBeInTheDocument()
})
})
})
Lecture - 14 - Code coverage
Code coverage:
A metric that can help you understand how much of your software code is tested
- Statement coverage: how many of the statements in the software code have been executed
- Branches coverage: how many of the branches of the control structures (if statements for instance) have been executed
- Function coverage: how many of the functions defined have been called
- Line coverage: how many of lines of source code have been tested
To obtain code coverage of your project, add a new script in package.json:
yarn test --coverage
As this command will take time to execute, we dont want to make it part of yarn test. hence we re adding a separate command.
While calulating coverage we need to consider all files in our project and (not files changed since last commit). That's why --watchAll flag is added.
--collectCoverageFrom: Only files inside src/components folder and with an extension of ts & tsx will be considered to get the coverage report. which will
include only App.tsx and not App.test.tsx
"yarn test --coverage --watchAll --collectCoverageFrom='src/components/**/*.{ts,tsx}'"
If you want to exclude a file while calculating coverage report:
"yarn test --coverage --watchAll --collectCoverageFrom='src/components/**/*.{ts,tsx}' --collectCoverageFrom='!src/components/**/*.{types, stories,
constants, test, spec}.{ts, tsx}'"
Note: For calculating coverage report we don't even need the test/spec files.
Coverage Threshold:
Coverage includes only our component files. So if our code coverage is less than a ceratin threshold, we can JEST will fail.
Add this object to package.json to mention your custom coverage threshold.
"jest": {
"coverageThreshold": {
"global":{
"branches": 80,
"functions": 80,
"lines": 80,
"statements": -10
}
}
}
This object tells JEST that if our project has
branch coverage less than 80
or
function coverage less than 80
or
lines coverage less than 80
or
if there are more than 10 uncovered statements, the overall test will fail.
Example:
Let's check for branch coverage for our project.
Now our greet component has two branches we can say. We have used a ternary operator. Because of that we have two branches.
1st Branch: If we pass name
2nd Branch: If we dont pass name
Now if we dont test for both of these branches in your greet.test.js:
then only 1 out 2 branch code is covered, which will result to 50% branch coverage.
I have commented
describe('Nested', () => {
test('renders with a name', () => {
render(<Greet name="Sailesh"/>)
const textEl = screen.getByText("Hello Sailesh");
expect(textEl).toBeInTheDocument()
})
which will give 50% coverage now.
// "coverage": "npx test --coverage --watchAll --collectCoverageFrom='src/components/**/*.{ts,tsx}'"
Note: 100% code coverage does not mean you have written tests covering critical parts of your application. Hence code coverage % can not be a metric to know
how good you re confident about your code.
Standard % is 80% though.
Lecture - 15 - Assertions:
Assertions:
- When writing tests, we often need to check that values meet certain conditions.
Example:
```render(<Greet />)
const textEl = screen.getByText("Hello");
expect(textEl).toBeInTheDocument()```
By this we re asserting Hello to be in te document.
- Assertions decide if a test passes or fails.
- Assertions can be tried out in JEST using expect function. Look at function signature below:
expect(value)
The arguement should be the value that your code produces.
Typically, you will use expect along with a "matcher" function to assert something about a value.
A matcher can optionally accept an arguement which is the correct expected value.
In our example, toBeInTheDocument: is the matcher func.
textEl is the value.
Find all matcher functions available in JEST here:
https://www.jestjs.io/docs/using-matchers
It have matchers for logical operations and not to test a dom element.
For ex; toBeGreaterThan(), toEqual()
For matching dom element, we have another package called jest-dom.
For ex: toBeInDom()
https://www.github.com/testing-library/jest-dom
Note:
jest-dom is installed for us in our react project by create-react-app.
check src/setupTests.js where we have imported jest-dom.
setupTests.js runs before jest runs the tests.
Lecture - 16 - What to test?
- What to test:
- Test component renders or not
- Test component renders with props.
- Test component renders in different state.
for ex: Loggedin & loggedout state
- Test component reacts to events.
for ex: Buttons & form controls.
- What not to test:
- Implementation details:
- This is based react-testing-library philosophy.
- The more your tests resemble the way your software is used, the more confidence that can give you.
- You want tests testing the behaviour and not how that behaviour is implemented.
- This makes refactoring easier.
- Third party code:
- Test the code written by you & not by any 3rd party lib like material-ui.
- Code that is not important from user's point of view.
- If you have a utility function which formats date values to a particular format, you dont have to test whther the utility func is called or not. Instead you
can directly check whether date value rendered in expected format or not.
Lecture - 17 - RTL Queries:
Every test we write generally involves the following basic steps:
1. Render the component
2. Find an element rendered by the component
3. Assert against the element found in step 2 which will pass or fail the test.
To render the component, we use the render method from RTL.
For assertion, we use expect passing in a value and combine it with a matcher function from jest or jest-dom.
This lecture is about the 2nd step: "Find an element rendered by the component"
--
RTL Queries: Queries are the methods that Testing Library provides to find the elements on the page.
To find a single element on the page, we have:
- getBy..
- queryBy..
- findBy..
To find multiple elements on the page, we have:
- getAllBy..
- queryAllBy..
- findAllBy..
The two says that each of these methods needs to be combined with a suffix to form the actual query.
The suffix can be one of the following:
- Role
- LabelText
- PlaceHolderText
- Text
- DisplayValue
- AltText
- Title
- TestId
--
getBy.. queries:
getBy.. class of queries return the matching node for a query, and throw a descriptive error if no elements match or if more than one match is found.
- getByRole
- getByLabelText
- getByPlaceHolderText
- getByText
- getByDisplayValue
- getByAltText
- getByTitle
- getByTestId
**- All these methods are available on screen object from @testing-library/react.
--
Lecture - 18 - getByRole
- By default many semantic elements in HTML have a role.
for ex: Button eleemnt has a button role, h1-h6 has a heading role etc
- If you're working wih elements that do not have a default role or if you want to specify a different role, the role attribute can be used to add the desired role.
- To use an anchor element as a button in the navbar, you can add role="button".'
To know roles of each html element, refer to:
https://www.w3.org/TR/html-aria/#docconformance
Lecture 27 - Priority Order for Queries:
Your test should resemble how users interact with your code (component, page, etc.) as much as possible.
1. getByRole
2. getByLabelText
3. getByPlaceHolderText
4. getByText
5. getByDisplayValue
6. getByAltText
7. getByTitle
8. getByTestId
Lecture 28 - Query Multiple elements
1. getAllByRole
2. getAllByLabelText
3. getAllByPlaceHolderText
4. getAllByText
5. getAllByDisplayValue
6. getAllByAltText
7. getAllByTitle
8. getAllByTestId
Example:
import { render, screen } from '@testing-library/react'
import { Skills } from './Skills'
describe('Skills', () => {
const skills = ['HTML', 'CSS', 'JavaScript']
test('renders correctly', () => {
render(<Skills skills={skills} />)
const listElement = screen.getByRole('list')
expect(listElement).toBeInTheDocument()
})
test('renders a list of skills', () => {
render(<Skills skills={skills} />)
const listItemElements = screen.getAllByRole('listitem')
expect(listItemElements).toHaveLength(skills.length)
})
});
Lecture 29 - TextMatch
RTL Queries used so far to find a single element or multiple elements in a dom:
const pageHeading = screen.getByRole("heading");
const nameElement2 = screen.getByLabelText("Name");
const nameElement3 = screen.getByPlaceHolderText("Fullname");
const paragraphElement = screen.getByText("All fields are mandatory");
const nameElement4 = screen.getByDisplayValue("Vishwas");
const imageElement = screen.getByAltText("a person with a laptop");
const closeElement = screen.getByTitle("close");
const customElement = screen.getByTestId("custom-element");
const listItemElements = screen.getAllByRole("listitem");
In this lecture, we are going to discuss about the 1st arguement we pass to each of these query methods:
At first glance, it may seem that the 1st arguement is of type "string", but it is not.
The type of the 1st arguement is of "TextMatch".
TextMatch:
TextMatch represents a type which can be either a
- string
- Regex
- function
TextMatch as String :
<div>Hello World</div>
screen.getByText('Hello World') // full string match
screen.getByText('llo Worl', {exact:false}) // substring match
screen.getByText('hello world', {exact: false}) // ignore case
TextMatch as Regex :
<div>Hello World</div>
screen.getByText(/World/) // substring match
screen.getByText(/world/i) //substring match, ignore case
screen.getByText(/^hello world$/i) // full string match, ignore case
TextMatch as Custom Function:
Custom function signature should be as follows:
(content?: string, element?: Element | null) => boolean
- Both content & element are optional
- Returns true or false
<div>Hello World</div>
screen.getByText(content => content.startsWith('Hello'))
screen.anyGetByOrGetAllByQueries(customFunction)
Example:
// substring match
const paragraphElement1 = screen.getByText("All", { exact: false });
expect(paragraphElement1).toBeInTheDocument();
// regex match
const paragraphElement2 = screen.getByText(/all fields are mandatory/i);
expect(paragraphElement2).toBeInTheDocument();
// function match
const paragraphElement3 = screen.getByText((content) => {
return content.startsWith("All");
});
expect(paragraphElement3).toBeInTheDocument();
--
Lecture 29 - queryBy
queryBy.. - We use queryBy queries to test whether a certaian HTML element is not rendered in DOM.
/**
*
* Note:
* All getBy.. & getAllBy.. queries will throw an error, If matching element is not found.
* This is scenario you can take help of queryBy.. and queryAllBy.. functions provided by JEST.
*
*
* test('Start Learning button is not rendered', () => {
* render(<Skills skills={skills} />)
* // const startLearningButton = screen.getByRole('button', {
* const startLearningButton = screen.queryByRole('button', {
* name: 'Start learning',
* })
* expect(startLearningButton).not.toBeInTheDocument()
* })
* */
--
import { useState, useEffect } from 'react'
import { SkillsProps } from './Skills.types'
export const Skills = (props: SkillsProps) => {
const { skills } = props
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
setTimeout(() => {
setIsLoggedIn(true)
}, 1001)
}, [])
return (
<>
<ul>
{skills.map((skill) => {
return <li key={skill}>{skill}</li>
})}
</ul>
{isLoggedIn ? (
<button>Start learning</button>
):(
<button onClick={() => setIsLoggedIn(true)}>Login</button>
)}
</>
)
}
--
Definitions of queryBy.. & queryAllBy.. -
- queryBy.. :
Returns the matching node for a query, and return null if no elements match
Useful for asserting an element that is not present
Throws an error if more than one match is found
- queryAllBy.. :
Returns an array of all matching nodes for a query and return an empty array If no elements match.
--
RTL queryBy and queryAllBy queries:
1. queryByRole
2. queryByLabelText
3. queryByPlaceHolderText
4. queryByText
5. queryByDisplayValue
6. queryByAltText
7. queryByTitle
8. queryByTestId
1. queryAllByRole
2. queryAllByLabelText
3. queryAllByPlaceHolderText
4. queryAllByText
5. queryAllByDisplayValue
6. queryAllByAltText
7. queryAllByTitle
8. queryAllByTestId
Lecture 31 - findBy
We have discussed till now:
getBy and getAllBy class of queries to assert if elements are present in the DOM
queryBy and queryAllBy class of queries to assert if elements are not present in the DOM
Let's discuss findBy:
Appearance / Disappearance:
What if elements are not present in the DOM to begin but make their way into the DOM after some time?
For example. data that is fetched from a server will be rendered only after a few miliseconds.
--
test('Start Learning button is eventually displayed', async () => {
render(<Skills skills={skills} />)
// const startLearningButton = await screen.getByRole(
const startLearningButton = await screen.findByRole(
'button',
{
name: 'Start learning',
},
{
timeout: 1002,
}
)
expect(startLearningButton).toBeInTheDocument()
})
--
findBy.. and findAllBy..
findBy:
- Returns a promise which resolves when an element is found which matches the given query
- The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms
findAllBy:
- Returns a promise which resolves to an array of elements when any elements are found which match the given query.
Note: The second arguement's default value is 1000ms, If you want to change default value, you can pass an object with timeout key.
Lecture 32 - Manual Queries:
Although react-testing-library provides 3 types of RTL queries, we can make use of DOM Api to query the dom elements in JEST.
Manual queries:
const {container} = render(<MyComponent />)
const foo = container.querySelector("[data-foo="bar"]")
Although manual queries using DOM Api works, using querySelector to query by class and id attribute etc is not recommended. Because these attributes are
not visible to the user.
You should always try to query using the 3 query types provided by react-testing-library.
This is recommended in official documentation.
Lecture 33 - Debugging:
In this video let's take a look at debugging with react testing library
Similar to application code sometimes the tests you write may not work the way you expect. Debugging is what will help us in such situations. But what does it
mean to debug a test? So, if you can get a glimpse of what the dom looks like we can figure out why our assertion is failing. One of the things that's great about
react testing library is that whenever there is an error in a test it provides a meaningful error message along with a formatted state of the dom tree created by the
render method.
In skills.test.tsx i'm going to modify the last test to fail. Change "Start learning" to "Start learn". If we save the file our tests rerun we can see the test group
name followed
by the test name where the error occurred.
We also have the error message "unable to find role is equal to button and name "start learn"
We then have a formatted state of the dom tree. We can easily inspect this dom tree and see that a button with text start learn is in fact not present which is
why our test fails.
Now although react testing library has this capability sometimes it is helpful if you can visualize the dom tree before writing the assertion to help with that we
can make use of the debug method on the screen object
I'm going to revert this back to learning and just before and after the find by role method I'm going to add screen dot debug.
If i save the file you can see the two dom trees printed in the terminal the first one right before find by where start learning button is not present, and the
second one after find by which has in fact waited for more than a second and this dom tree contains the start learning button
Being able to visualize the dom at any given point in your test is really helpful to debug and understand why your test might be failing
debug():
Example:
test('Start Learning button is eventuall displayed', async () => {
render(<Skills skills={skills} />)
screen.debug()
const startLearningButton = await screen.findByRole(
'button',
{
name: 'Start learning',
},
{
timeout: 1002,
}
)
screen.debug()
expect(startLearningButton).toBeInTheDocument()
})
logRoles:
The logRoles function can be used to print out a list of all the implicit aria roles within a tree of dom nodes.
Each role containing a list of all the nodes which match that role.
Example:
import { render, screen, logRoles } from '@testing-library/react';
test('Start Learning button is eventually displayed', async () => {
const view = render(<Skills skills={skills} />)
logRoles(view.container)
const startLearningButton = await screen.findByRole(
'button',
{
name: 'Start learning',
},
{
timeout: 1002,
}
)
expect(startLearningButton).toBeInTheDocument()
})
Lecture 34 - Testing Playground
Lecture 35 - User Interactions:
User Interactions:
A click using a mouse or a keypress using a keyboard.
Our code has to respond to such interactions.
Tests should ensure the interactions are handled as expected.
We'll use a library called user-event for this.
user-event:
A companion library for Testing Library that simulates user interactions by dispatching the events that would happen if the interaction took place in
browser.
It is recommended way to test user interactions with RTL.
fireEvent vs user-event:
- fireEvent is a method from RTL which is used to dispatch DOM events.
- user-event simulates full interactions, which may fire multiple events and do additional checks along the way.
- For example, we can dispatch the change event on an input field using fireEvent
- When a user types into a text box, the element has to be focused, and then keyboard and input events are fired and the selection and value on the
element are manipulated as they type.
- user-event allows you to describe a user interaction instead of concrete event. It adds visibility and interactability checks along the way and manipulates
the DOM just like a user interaction in the browser would. It factors in that the browser e.g. wouldn't let a user click a hidden element or type in a disabled text
box.
Lecture 36 - Pointer Interaction or Mouse interaction:
- counter component in our demo project
- click() is not a pointer api, but is a convenience api that internally calls Pointer apis.
- convenience apis are the apis that we call while writing tests.
- All the methods we call on user-events are asynchronous.
Example:
import { render, screen } from '@testing-library/react'
import { Counter } from './Counter'
import user from '@testing-library/user-event'
test('renders a count of 1 after clicking the increment button', async () => {
user.setup()
render(<Counter />)
const incrementButton = screen.getByRole('button', { name: 'Increment' })
await user.click(incrementButton)
const countElement = screen.getByRole('heading')
expect(countElement).toHaveTextContent('1')
})
--
convenience APIs:
- click()
- dblClick()
- tripleClick()
- hover()
- unhover()
Apart from the above convenience apis, we can also call the low-level pointer apis:
pointer({keys: '[MouseLeft]'})
to simulate left-click event
pointer({keys: '[MouseLeft][MouseRight]'})
to simulate left-click event and a right click followed by.
pointer({'[MouseLeft][MouseRight]'})
If keys is the only thing you want to pass in object, this is a shorthand notation for that.
pointer({keys: '[MouseLeft>]'})
to simulate only pressing of a button, without releasing it, suffix event name with >
pointer({keys: '/MouseLeft]'})
to simulate releasing of a previously clicked button.
Lecture 36 - Keyboard Interactions:
Example:
import { render, screen } from '@testing-library/react'
import { Counter } from './Counter'
import user from '@testing-library/user-event'
test('rendres a count of 10 after clicking the set button', async () => {
user.setup()
render(<Counter />)
const amountInput = screen.getByRole('spinbutton') // spinbutton is the role for number type inputs
await user.type(amountInput, '10')
expect(amountInput).toHaveValue(10)
const setButton = screen.getByRole('button', { name: 'Set' })
await user.click(setButton)
const countElement = screen.getByRole('heading')
expect(countElement).toHaveTextContent('10')
})
test('elements are focused in the right order when clicked tab', async () => {
user.setup()
render(<Counter />)
const amountInput = screen.getByRole('spinbutton')
const setButton = screen.getByRole('button', { name: 'Set' })
const incrementButton = screen.getByRole('button', { name: 'Increment' })
await user.tab()
expect(incrementButton).toHaveFocus()
await user.tab()
expect(amountInput).toHaveFocus()
await user.tab()
expect(setButton).toHaveFocus()
})
--
Theory:
- type() & tab() are not part of keyboard API.
- type() is an utility api and tab() is a convenience api.
- tab() is only one convenience api, while there are many utility apis available.
- Other common utility apis:
- clear(): To clear an editable element
Example:
test('clear', async () => {
render(<textarea defaultValue="Hello, World!" />)
await userEvent.clear(screen.getByRole('textbox'))
expect(screen.getByRole('textbox')).toHaveValue('')
})
- selectOptions() & deselectOptions() :
Used to easily select or deselect options in dropdown
or
Select multiple values in a listbox, where we can select multiple elements.
Example of multi-select:
test('selectOptions', async () => {
render(
<select multiple>
<option value="1">A<option/>
<option value="2">B<option/>
<option value="3">C<option/>
</select>
)
await userEvent.selectOptions(screen.getByRole('listbox'), ['1', 'C']) // select by value or label
expect(screen.getByRole('option', {name: 'A'}).selected).toBe(true)
expect(screen.getByRole('option', {name: 'B'}).selected).toBe(false)
expect(screen.getByRole('option', {name: 'C'}).selected).toBe(true)
})
- upload():
Used to change a file input as if a user clicked it and selected a files in the resulting file upload dialog
Example:
test('upload file', async () => {
render(
<div>
<label htmlFor="file-uploader"> Upload file:</label>
<input id="file-uploader" type="file" />
</div>
)
const file = new File(['hello'], 'hello.png', {type:'image/png'})
const input = screen.getByLabelText(/upload file/i)
await userEvent.upload(input, file)
expect(input.files[0]).toBe(file);
expect(input.files.item[0]).toBe(file);
expect(input.files).toHaveLength(1);
})
Apart from convenience & utility apis, we also have clipboard apis:
copy()
cut()
paste()
Low-level keyboard apis:
keyboard('foo') - simulates clicking of 'f', followed by 'o', followed by another 'o' in keyboard.
keyboard('{Shift>}A{/shift}') - simulates to shift key press(down), then A key press and then Shift release (up)
Lecture 38 - Providers
Testing providers.
At the very root level of our application, we wrap our app or app.component with all the providers we want, for ex: Redux store provider, or our own custom
Auth provider or any 3rd party css provider (Material UI Provider).
We wrap because we want these data available to all our components.
That's why while writing our tests for a component, we need to provide a wrapper to it.
or
Otherwise, as the provider is required to be wrapped by all our components, we can do a setup for this globally, so that we don't have to worry about it in every
single component.
Example:
app-providers.tsx
--
import { ThemeProvider, createTheme } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
const theme = createTheme({
palette: {
mode: 'dark',
},
})
export const AppProviders = ({ children }: { children: React.ReactNode }) => {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
)
}
--
App.tsx
import './App.css'
import { Application } from './components/application/Application'
import { CounterTwo } from './components/counter-two/CounterTwo'
import { Counter } from './components/counter/Counter'
import { MuiMode } from './components/mui/MuiMode'
import { Skills } from './components/skills/Skills'
import { Users } from './components/users/Users'
import { AppProviders } from './providers/AppProviders'
function App() {
return (
<AppProviders>
<div className="App">
<Application />
<Skills skills={['HTML', 'CSS']} />
<Counter />
<CounterTwo count={1} />
<Users />
<MuiMode />
</div>
</AppProviders>
)
}
export default App
--
MuiMode.tsx
import { useTheme } from '@mui/material/styles'
import { Typography } from '@mui/material'
export const MuiMode = () => {
const theme = useTheme()
return (
<>
<Typography component="h1">{`${theme.palette.mode} mode`}</Typography>
</>
)
}
--
MuiMode.test.tsx
import { render, screen } from '../../test-utils'
import { MuiMode } from './MuiMode'
import { AppProviders } from '../../providers/AppProviders'
describe('MuiMode', () => {
test('renders text correctly', () => {
render(<MuiMode />, {
wrapper: AppProviders
})
const headingElement = screen.getByRole('heading')
expect(headingElement).toHaveTextContent('dark mode')
})
})
Lecture 39 - Custom Render Functions:
Setting up providers globally for all components
https://testing-library.com/docs/react-testing-library/setup
--
my-component.test.jsx
- import { render, fireEvent } from '@testing-library/react'; // render func provided by testing-library
+ import { render, fireEvent } from '../test-utils'; // custom render func
--
test-utils.jsx
import React from 'react'
import {render} from '@testing-library/react'
import { AppProviders } from './providers/AppProviders'
const customRender = (
ui, options) =>
render(ui, {
wrapper: AppProviders,
...options
}
)
// re-export everything
export * from '@testing-library/react'
export {customRender as render}
--
Lecture 40 - Tesing Custom React Hooks
Passing a hook to render method like we pass react compnents, will throw an error.
Because a custom hook doesn't return any JSX. Also custom hook can not be called outside a function component.
How do we test a custom hook then?
renderHook() from @testing-library/react.
renderHook takes custom hook as an arguement.
Unlike regular react components which can be asserted using "screen", hooks do not have any DOM elements.
Instead renderHook will wrap the hook in a function component, invoke the hook and returns an object from which we can destructure a property called
"result".
"result" has a property known as "current", which will conatin all returned value from custom hook.
which can be used for assertion.
Example:
useCounter.types.ts
export type UseCounterProps = {
initialCount?: number
}
useCounter.tsx
import { useState } from 'react'
import { UseCounterProps } from './userCouner.types'
export const useCounter = ({ initialCount = 0 }: UseCounterProps = {}) => {
const [count, setCount] = useState(initialCount)
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
return { count, increment, decrement }
}
useCounter.test.tsx
import { renderHook } from '@testing-library/react'
import { useCounter } from './useCounter'
describe('useCounter', () => {
test('should render the initial count', () => {
const { result } = renderHook(useCounter)
expect(result.current.count).toBe(0)
})
test('should accept and render the same initial count', () => {
const { result } = renderHook(useCounter, {
initialProps: {
initialCount: 10
}
})
expect(result.current.count).toBe(10)
})
})
Lecture 41 - Act Utility
act() -
When writing UI tests, tasks like rendering, user events, or data fetching can be considered as “units” of interaction with a user interface. react-dom/test-utils
provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions
This helps make your tests run closer to what real users would experience when using your application. The rest of these examples use act() to make these
guarantees.
You might find using act() directly a bit too verbose. To avoid some of the boilerplate, you could use a library like React Testing Library, whose helpers are
wrapped with act().
Note:
The name act comes from the Arrange-Act-Assert pattern.
useCounter.test.tsx
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
describe('useCounter', () => {
test('should render the initial count', () => {
const { result } = renderHook(useCounter)
expect(result.current.count).toBe(0)
})
test('should accept and render the same initial count', () => {
const { result } = renderHook(useCounter, {
initialProps: { initialCount: 10 },
})
expect(result.current.count).toBe(10)
})
test('should increment the count', () => {
const { result } = renderHook(useCounter)
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})
test('should decrement the count', () => {
const { result } = renderHook(useCounter)
act(() => result.current.decrement())
expect(result.current.count).toBe(-1)
})
})
Lecture 42 - Mocking Functions:
Mocking functions in JEST:
CounterTwo.tsx
import { CounterTwoProps } from './CounterTwo.types'
export const CounterTwo = (props: CounterTwoProps) => {
return (
<div>
<h1>Counter Two</h1>
<p>{props.count}</p>
{props.handleIncrement && (
<button onClick={props.handleIncrement}>Increment</button>
)}
{props.handleDecrement && (
<button onClick={props.handleDecrement}>Decrement</button>
)}
</div>
)
}
CounterTwo.types.tsx
export type CounterTwoProps = {
count: number
handleIncrement?: () => void
handleDecrement?: () => void
}
--
From above, it says that CounterTwo is a props-driven presentational component.
From CounterTwo.types.tsx:
It says that the component receives a count props and two optional functions in props
CounterTwo.tsx, renders two buttons increment & decrement; only if, is corresponding props is passed to it and then attach as a click listener to those button.
When we will write test for CounterTwo.tests.tsx, we don't have any idea about by what amount the parent func will increment the counter. In this scenario,
we don't care what CounterTwo.tsx receives from its parent, we just have to mock that the click handler given to us from parent component is called. We call this
as Mocking.
CounterTwo.tests.tsx
import { render, screen } from '@testing-library/react'
import user from '@testing-library/user-event'
import { CounterTwo } from './CounterTwo'
test('renders correctly', () => {
render(<CounterTwo count={0} />)
const textElement = screen.getByText('Counter Two')
expect(textElement).toBeInTheDocument()
})
test('handlers are called', async () => {
user.setup()
const incrementHandler = jest.fn()
const decrementHandler = jest.fn()
render(
<CounterTwo
count={0}
handleIncrement={incrementHandler}
handleDecrement={decrementHandler}
/>
)
const incrementButton = screen.getByRole('button', { name: 'Increment' })
const decrementButton = screen.getByRole('button', { name: 'Decrement' })
await user.click(incrementButton)
await user.click(decrementButton)
expect(incrementHandler).toHaveBeenCalledTimes(1)
expect(decrementHandler).toHaveBeenCalledTimes(1)
})
Lecture 43 - Making HTTP Requests
Users.tsx:
import { useState, useEffect } from 'react'
export const Users = () => {
const [users, setUsers] = useState<string[]>([])
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((res) => res.json())
.then((data) => setUsers(data.map((user: { name: string }) => user.name)))
.catch(() => setError('Error fetching users'))
}, [])
return (
<div>
<h1>Users</h1>
{error && <p>{error}</p>}
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
</div>
)
}
The above component, fetches data from an api and renders it in UI. If there's failure in fetching of data, we show an error message.
Our goal is to write a test which checks this components renders a list of users or an error msg if any
Note:
Real apis are primarily used only for End-to-End tests and not for unit or functional tests
Reason:
1. We don't have to ensure, the server is up and running to test whether the component renders as intended.
2. Since these tests are run quite often, it is not feasible to include real apis, which may even charge you based on number of requests.
What we have to do instead is Mock the HTTP requests in our test.
In our case, we mock the response to the request with a list of users or an error.
For mocking when writing tests with react-testing-library, we will use the package mock-service-worker ie. msw
msw is an api mocking library that uses service-worker API, to intercept actual requests. It is the closest thing to mocking a server without having to create one.
Lecture 44 - MSW Setup
https://www.mswjs.io
Lecture 45 - MSW Handlers
Lecture 48 - Static Analysis Testing
Process of verifying that your code meets certain expectations without actually running it.
- Ensure consistent style and formatting
- Check for common mistakes and possible bugs
- Limit the complexity of code and
- Verify type consistency
All types of testing like unit testing, integration testing or functional testing, run the code and then compare the outcome against known expected outputs to
see if everything works ok.
Static testing analyses aspects such as readability, consistency, error handling, type checking and alignment with best practices.
Testing checks if your code works or not, whereas static analysis checks if it is written well or not.
Static analysis testing tools:
- Typescript: Throw error if expected is a string and passed a number
- ESlint
- Prettier
- Husky
- lint-staged
Lecture 49 - ESlint
ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs.
Lecture 50 - Prettier
Prettier is an opinionated code formatter that ensures that all outputted code conforms to a consistent style.
Lecture 51 - Husky
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment