Berat Bozkurt

Berat Bozkurt




Let's Write Tests for React Application

08 Nisan 2023 • 1 view•
Let's Write Tests for React Application
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

  • Testing Library Doc
  • Writng Your First E2E Test - Cypress
  • React Testing Library Tutorial – How to Write Unit Tests for React Apps - FreeCodeCamp
  • React Testing Library Tutorial - Robin Wieruch
  • Unit Testing of React Apps using JEST - BrowserStack
  • How Do We Test Our Front-End Applications in Trendyol GO?
  • Jest Mocking Strategies - Mercedes Bernard
  • Test Test Diye Nice Nice: Mocklama - Enes Başpınar
  • Testing API Calls in useEffect using React Testing Library and Jest - Tania Rascia
•
Share on X (Twitter)