Last active
January 10, 2023 13:05
-
-
Save sailesh97/b80e927e04ae1e43e93f22d8df9e4708 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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