So at a pretty basic level you could write tests just like :
function add(a. b) {
return a + b;
}
function test () {
console.assert(add(1, 1) === 2)
console.assert(add(-1, 1) === 0)
console.assert(add(1, -1) === 0)
}
test();
It doesn’t give you a lot of information, but it’ll sorta work, you could expand on that, add messages, make it a bit nicer to write, make it look nice in the console, etc.
Then if you had lots of those tests, you’d want something that ran them all automatically. So to take Jest as an example, it is what’s called a test runner. Jest is preconfigured to do a lot of stuff (not just run tests), but basically, out of the box:
When you run it, it will look for files called somefile.test.js
or files in a folder called __test__
.
In those files, anything inside a block of code like this:
test("a description of the test", () => {
// a test here
})`
// Or like this:
it("should have a description here", () => {
// a test here
})
(Those are the same, it’s just what makes most sense when you’re reading it)
Will be ran. At a basic level, for unit tests (which test a unit of code, normally one function), you just test that given some input, you get some output.
test("1 plus 1 is 2", () => {
return add(1, 1) === 2;
});
Jest includes a library of what’s called assertions, which make the tests a bit easier to read, basically. So there’s a function expect
, and can use it so the test looks like
test("1 plus 1 is 2", () => {
expect(add(1, 1)).toBe(2);
});
For React, the components are what you want to test. And the components are functions, so you can unit test. But there’s a load of stuff React does that you don’t want to worry about: you normally just want to say "if I give that component these props it will render this. So you can use a testing library for this, and the react-testing-library is kinda now the de facto standard for that. So using it, the tests look like
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { HelloThing } from "./HelloThing";
it("renders hello world if no name is given", () => {
const { getByText } = render(<HelloThing />);
expect (getByText("Hello, World!")).toBeInTheDocument();
});
it("renders hello {name} if a name is given", () => {
const { getByText } = render(<HelloThing name="Dan" />);
expect (getByText("Hello, Dan!")).toBeInTheDocument();
});
Unit tests are easy to write, and often if you were testing they would make up the majority of the tests. But they only test things in isolation, that being the point of them. They don’t tell you how larger parts of the system behave.
Integration tests are for testing if things work when you put those smaller functions together, often when they’re running against some other part of the system. You may already know how they work individually, but then you check if they function in the groups they appear in in the system. They’re harder to write than unit tests, and a little bit more fragile (if one of the units of code is changed, it may well break the integration tests).
End to end tests are where you test the entire application from outside. For these tests, how it works internally doesn’t matter. For web applications, it means automating a browser so that it acts like a user, clicking things, filling in fields, etc. So there are frameworks to help with this – Cypress is one that is used a lot. E2E tests are often the most fragile and most painful to write and maintain, and by far the slowest to run, but they are the only ones that fully test the end application works from a user perspective.
Again, both of these types of tests would normally be ran using a test runner like Jest.
There are other types of tests. For example I’m currently writing model-based tests, where I describe a set of states the UI can be in and the events that trigger changes, then it just computes every possible valid combination and runs tests to make sure I can get to the final state of each combination. And for simple library functions, I tend to want property based tests, where I test some property of the function (eg a reverseString function output should be the same length as the input, or should have the same characters, or those characters should be in reverse order) by giving it every single possible input until it breaks. And there are loads of other testing strategies that work well for different situations.
For you, with React, Jest, with the react testing library, doing mainly unit tests, that’s a very common setup – you get that in CRA, for example
Edit: With your API, your using a thing designed for testing: JSON server. So components that make use of it directly via fetch requests, you can specify exactly what those fetch requests are going to resolve to, and in turn you can run what will be integration tests, still saying “when I render this component, it will look like that”. Jest has stuff to help with these scenarios, and there are lots of test libraries that can help as well, anyway. I’d get used to testing by writing against components that just take props and render something first though