Formik is a library that makes creating complex forms a snap. Testing Library (previously known as React Testing Library) is the gold standard when it comes to testing React applications. While working with Formik for the last couple of years, I have found that some developers are not comfortable with testing it.

For this exercise, we will make a simple React application using create-react-app that renders a login form, has some simple validation and makes an API call to verify the username and password. If the API tells us that the username and password are correct, our application should redirect the user to another page. If incorrect, we should display a message to the user to that effect.

Building our application

We can start by bootstrapping a new React application with create-react-app. I decided to use the TypeScript template.

npx create-react-app my-app --template typescript

**** NOTE ****

Before getting started, make sure you update all of your dependencies to their latest versions. At the time of writing this article, there was a problem with using Create React App with testing library. To resolve this problem, I had to install jest-environment-jsdom-sixteen and update my test script.

"test": "react-scripts test --env=jest-environment-jsdom-sixteen"

Next, I created a file called LoginForm, and added the following.

import React from "react";
import cx from "classnames";
import { Formik } from "formik";

export const LoginForm = () => {
  return (
    <Formik
      initialValues={{ email: "", password: "" }}
      validate={values => {
        const errors: Record<string, string> = {};
        if (!values.email) {
          errors.email = "Required";
        } else if (
          !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
        ) {
          errors.email = "Invalid email address";
        }
        if (!values.password) {
          errors.password = "Required";
        }
        return errors;
      }}
      onSubmit={async (values, { setStatus, setSubmitting }) => {
        setSubmitting(true);
        try {
          const response = await fetch("/api/sign-in", {
            method: "POST", // or 'PUT'
            headers: {
              "Content-Type": "application/json"
            },
            body: JSON.stringify(values)
          });

          if (!response.ok) {
            throw Error(response.statusText);
          }

          window.location.replace("/dashboard");
        } catch (e) {
          console.log("e", 
          e);
          const apiError = {
            apiError: "Invalid username or password"
          };
          setStatus(apiError);
        } finally {
          setSubmitting(false);
        }
      }}
    >
      {({
        values,
        status,
        errors,
        touched,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting
        /* and other goodies */
      }) => {
        const hasError = Object.keys(errors).length > 0;
        return (
          <div className="w-full max-w-xs">
            {status && status["apiError"] && (
              <div
                className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4"
                role="alert"
              >
                 {status["apiError"]}
              </div>
            )}

            <form
              className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
              onSubmit={handleSubmit}
            >
              <div className="mb-4">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="email"
                >
                  Email
                </label>
                <input
                  className={cx(
                    "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline",
                    {
                      "border-red-500": touched["email"] && errors["email"]
                    }
                  )}
                  id="email"
                  name="email"
                  type="text"
                  placeholder="Email"
                  value={values.email}
                  onChange={handleChange}
                  onBlur={handleBlur}
                />
                {touched["email"] && errors["email"] && (
                  <p className="pt-2 text-red-500 text-xs italic">
                    {errors["email"]}
                  </p>
                )}
              </div>
              <div className="mb-6">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="password"
                >
                  Password
                </label>
                <input
                  className={cx(
                    "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline",
                    {
                      "border-red-500": touched["email"] && errors["email"]
                    }
                  )}
                  id="password"
                  name="password"
                  type="password"
                  value={values.password}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  placeholder="******************"
                />
                {touched["password"] && errors["password"] && (
                  <p className="text-red-500 text-xs italic">
                    {errors["password"]}
                  </p>
                )}
              </div>
              <div className="flex items-center justify-between">
                <button
                  disabled={isSubmitting || hasError}
                  className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
                  type="submit"
                >
                  Sign In
                </button>
                <a
                  className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"
                  href="#"
                >
                  Forgot Password?
                </a>
              </div>
            </form>
            <p className="text-center text-gray-500 text-xs">
              ©2020 Acme Corp. All rights reserved.
            </p>
          </div>
        );
      }}
    </Formik>
  );
};

There’s a lot going on in here, so let’s just concentrate on a few things.

  • We use Tailwind for some quick and dirty CSS styling.
  • We validate the email and password fields on the client-side and show errors if they are touched.
  • We call the API in our submit function, and use setStatus from Formik if we get an error, otherwise we use window.location.replace to redirect the user.

After playing around with our form in the browser, we can start by writing our first tests, but first, we need to decide what to test.

#react #javascript #programming #testing #developer

Testing Asynchronous React Forms with Formik and Testing Library
26.30 GEEK