
It was a long-awaited article series. Actually, I also wanted to do a few things to improve myself on testing. This article was an excuse for me to tell you. Well, as you know, if there is something you want to learn, you need to tell it to someone. You need to learn it better... That's why I'm here :)
Before starting the article, let me quickly pass the backlink. For those who haven't read the first article of the series, let's take it here. Let's Write Tests for React Application #1
We will get straight to the topic without going into what is testing, what is unit testing, what is integration testing. I developed a project to make it more memorable and shared it publicly on github. This way, you can examine the project and understand the subject of testing at least to some extent.
Actually, I tried to keep the project simple. This way, we can better focus on the topic we need to focus on. The project is a simple guestbook application. It is a project that works completely locally, has no API connection, and consists of a few components. You can review the live version of the project at Guestbook.
If you want to access the source codes on Github, you can reach them at github.com/berat/guestbook. I also took a screen recording while developing the project and shared it on YouTube.
Actually, I didn't know exactly which situations I could explore. But the ones that came to my mind are as follows;
I tried to get answers to questions like these. Of course, the test I wrote is still simple. More advanced than this would be to write tests with mock data in an application with redux connection and API connection and to test the store change. But of course, these are advanced topics. These will do the job for now.
The form where the comment will be written is the component
import { memo, useEffect, useState } from "react";
const INITIAL_STATE = {
name: "",
comment: "",
};
const Form = ({ setComments }) => {
const [values, setValues] = useState(INITIAL_STATE);
const [buttonDisabled, setButtonDisabled] = useState(true);
const handleChange = (event) => {
const { name, value } = event.target;
setValues({ ...values, [name]: value });
};
useEffect(() => {
const getValues = Object.values(values);
const isEmpty = getValues.some((item) => item === "");
if (isEmpty) setButtonDisabled(true);
else setButtonDisabled(false);
}, [values]);
const onSubmit = () => {
setComments((prev) => [
...prev,
{
id: Math.floor(Math.random() * 999999),
name: values.name,
comment: values.comment,
},
]);
// reset values
setValues(INITIAL_STATE);
};
return (
<div id="form">
<input
type="text"
placeholder="Name"
name="name"
value={values.name}
onChange={handleChange}
/>
<textarea
rows={2}
placeholder="Message"
name="comment"
value={values.comment}
onChange={handleChange}
></textarea>
<button onClick={onSubmit} disabled={buttonDisabled}>
Submit
</button>
</div>
);
};
export default memo(Form);Here is a simple component with an input, a textarea and a button. There are some functions required for this component to work. To summarize them;
handleChange function is used to save the values of the inputs and textareas to the state.useEffect hook checks whether the inputs and textareas are empty. If they are empty, the button is disabled.onSubmit function works when the button is clicked. Here, a new comment is added to the state with the setComments function. In addition, the values of the inputs and textareas are reset.The form where the comment will be written is the component test
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Form from "./Form";
describe("should render", () => {
test("input", () => {
render(<Form />);
const input = screen.getByPlaceholderText("Name");
expect(input).toBeInTheDocument();
});
test("textarea", () => {
render(<Form />);
const textarea = screen.getByPlaceholderText("Message");
expect(textarea).toBeInTheDocument();
});
test("button", () => {
render(<Form />);
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
});
});
test("case button dissable props", () => {
render(<Form />);
// get elements
const input = screen.getByPlaceholderText("Name");
const textarea = screen.getByPlaceholderText("Message");
const button = screen.getByRole("button");
expect(button).toBeDisabled();
userEvent.type(input, "test");
userEvent.type(textarea, "test");
expect(button).not.toBeDisabled();
});Don't get hung up on the explanations of the test cases. Use more explanatory and appropriate expressions :)
Here I proceed step by step as follows;
input, textarea and button elements are rendered. You can do these in a test scope, it will not be a problem. I wrote them separately because I wanted it to be a little more understandable. The operations done here are to call the component, call which element you want to check whether it is rendered and check it with the toBeInTheDocument function. If it is not rendered, it will fail anyway.input and textarea must be empty. So in the first stage, I fill in the values of these inputs. Then, I check whether the button is disabled with the not.toBeDisabled function. Here, I additionally changed the values of the inputs and textareas with the userEvent library.With the userEvent library, you can enter the input value, create a trigger or upload anything. You can review this page for detailed usage.
I didn't want to write the other component separately. Because they are similar structures, I explained them all from a single component. If you want to examine them, you can examine them in detail on github.
I think unit tests alone are not enough. Because components can be connected to each other. That's why it is necessary to write an end-to-end test with integration test. My preference here is Cypress. Because its writing and the opportunities it offers are 1-0 ahead for me.
After making the necessary installations, it's time to write the test. There are 4 steps in my test. These are;
describe("first test", () => {
it("post a comment", () => {
cy.visit("/"); // (1)
// fill inputs. (2)
cy.get('input[type="text"]').type("Berat Bozkurt");
cy.get("textarea").type("This is an example.");
// click the button (3)
cy.get("button").click();
// to check if the comment sented already. (4)
cy.get("article p").invoke("text").should("eq", "This is an example.");
});
});
All sources and documents are at the bottom of the text.
cy.visit function.cy.get function. cy.get(XX).type function.cy.get(xx).click function.cy.get(XX).invoke('text').should('eq', 'TEXT') function. Because the first parameter we send to the should function is eq.After completing the integration test writing process, the test process is carried out either via the terminal or the browser. My preference is to carry out the test process via the browser. Because when I carry out the test process via the browser, if there is an error, I encounter an error message when I encounter that error. This makes it more understandable for me.
Actually, we see that writing tests is not that difficult. We just need to allocate the necessary time for this and determine the cases. Then, we create a higher quality and more robust code structure. Because our cases are clear. If the component will undergo a revision in the future, the probability of creating a bug decreases greatly. Also, with test coverage, we can see how much the components we wrote have been tested. This increases the quality of the project.
Before finishing the article, you can follow me on my social media accounts below. If you have any questions, you can send me a message or a greeting without having to do it..