From this course.
What clicked for me regarding the importance of testing: When you refactor your code, it is easier to be guided by these tests to know you didn’t break anything in the process.
A test can pass simply by not failing, which means it might not actually verify the code’s correct functionality.
Just avoiding an error does not guarantee correct implementation.
A test fails when exception is thrown (either explicitly or because the expectation is not met).
TDD
The red, green, refactor pattern: Start with a failing test (red), implement code to make the test pass (green), and then refactor the code while maintaining the test’s success
Syntax:
// Describe is used for a suite of tests
describe("add", () => {
// it is used for a particular test case
it("should add two positive numbers", () => {
// here we can have multiple expectations to check for
expect(add(2, 4)).toBe(6);
});
});toBe() - ok for primitive values, like numbers and strings; not ok for objects, since they refer to different memory locations.
Use toEqual() for objects and array in that regard.
The Triple A pattern consists of Arrange (set up the test conditions), Act (perform the action being tested), and Assert (verify the expected outcomes).
react testing library - find vs get
Get immediately retrieves elements from the DOM, while find is asynchronous and waits for elements to appear.
Default timeout - 300ms
What makes the ‘findByLabel’ selector useful for testing form inputs: It can find them by their associated label text without needing a specific test ID
Test Doubles
Spies - wraps a function without changing its implementation.
One can check if how many times this function was called, with what arguments, what return values etc.
Mocks - wraps a function with a controlled implementation for testing purposes.
To replace a real function with a controlled implementation for testing purposes
Spying observes the original function, while mocking replaces the function’s behavior
TIP
Use dependency injection to pass controllable functions for handling externl dependencies in testing
One can use a combination of these two:
vi.spyOn(Math, "random").mockImplementation(() => 0.5);
// ...
const result = Math.random();
expect(result).toBe(0.5);Stub - A temporary replacement for a real value or function that is used during testing
Playwright
Spins up a browser and tests browser interactions, whole flows.
Nice idea: set up tests for all viewports you care about, let it record screenshots too, requests, and refactor with ease without fearing we broke certain views.
Mock Service Worker
Great to intercept network requests to get predefined responses.
Useful in testing and also when working on the front-end while the back-end isn’t ready.
Can use different responses for different test scenarios.
export const handlers = [
http.get("/api/tasks", async () => {
return HttpResponse.json(tasks);
}),
http.post("/api/tasks", async (req) => {
const { title } = await req.json();
const newTask = createTask(title);
tasks.push(newTask);
return HttpResponse.json(newTask, { status: 201 });
}),
];Conclusions
Honestly, this course made me think more about how I write the current code rather than the tests themselves. What Steve highlighted about writing testable code, using dependency injection as a pattern, kinda made me excited to refactor some recent code I wrote recently for the thesis.
The suggested tools are nice, despite currently lacking the ‘intuition’ one would have about what tests would be useful in what type of context.