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.
Let's Review the Project
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.
Let's Start with Unit Tests
Actually, I didn't know exactly which situations I could explore. But the ones that came to my mind are as follows;
Is the component rendering?
Do the disable/enable situations work correctly?
Do the input values work correctly?
Do the steps after the button clicks happen correctly?
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;
The handleChange
function is used to save the values of the inputs and textareas to the state.
The useEffect
hook checks whether the inputs and textareas are empty. If they are empty, the button is disabled.
The 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;
I check whether the 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.
Another test case is whether the button is disabled or not. In order for the button to be disabled, the 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.
Now it's time for Integration Test
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;
First, go to the address where the test will be written. In my example, it will be "/". (1)
Then, input and textarea will be selected and filled with the necessary values. (2)
After the values of input and textarea are entered, the button will be clicked. (3)
After the button is clicked, the entered data should actually appear on the screen. At this point, I check it with the HTML tag I provided. (4)
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.");
});
});
Tüm kaynaklar ve dökümanlar yazının en altında bulunmaktadır.
cy.visit
fonksiyonu ile adresimize gidiyoruz.
cy.get
fonksiyonu ile elemanlarımızı seçiyoruz.
cy.get(XX).type
fonksiyonu ile elemanların değerlerini giriyoruz.
cy.get(xx).click
fonksiyonu ile butona tıklıyoruz.
cy.get(XX).invoke('text').should('eq', 'TEXT')
fonksiyonu ile elemanın içerisindeki text ne ise TEXT
ile yazdığımız değer ile eş değer mi onu kontrol ediyoruz. Çünkü should
fonksiyonuna gönderdiğimiz ilk parametre eq.
Aslında test yazmak o kadar da zor olmadığını görüyoruz. Sadece bunun için gerekli zamanı ayırıp case'leri belirlememiz gerekmektedir. Daha sonra ise daha kaliteli ve sağlam bir kod yapısı oluşturmuş oluyoruz. Çünkü case'lerimiz belli. Eğer component ileride bir revize geçirecekse bug yaratma olasılığımız çok ama çok azalıyor. Hem de test coverage ile yazdığımız component'lerin ne kadar test edildiğini görebiliyoruz. Bu da projenin kalitesini arttırıyor.
All sources and documents are at the bottom of the text.
We go to our address with the cy.visit
function.
We select our elements with the cy.get
function.
We enter the values of the elements with the cy.get(XX).type
function.
We click the button with the cy.get(xx).click
function.
We check whether the text in the element is equal to the value we wrote with TEXT with the 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.
Conclusion
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..
Resources