AWS Amplify Unit Test

Hello, happy holidays, and well wishes for the end of this year and the start of the next. This transitional period going into 2024 has me feeling the need to tidy things up. Get rid of old, unused domains, sunset some legacy projects, that sort of thing. In the spirit of cleaning up my digital presence, I'm redesigning https://joeyreyes.rocks, my music site and counterpart to this site. It's currently a pretty rough-looking Gatsby site deployed to Netlify.

The biggest thing about the site is the shows archive, which is managed through Contentful. I am keeping this portion of the setup as-is for now, but adding in some new fields to my Show content model. I'm taking this opportunity to test out Contentful's environments feature.

The first architecture change I want to accomplish with this redesign is migrating from Gatsby to Next.js. Gatsby just seems like it's on its last leg, and Next.js seems to have won all of the market share and maturity, at least for the moment. We've also migrated to Next.js at work, so I'm already quite familiar with it.

The second thing I want to test out with this redesign is deployment alternatives to Netlify, specifically AWS Amplify. I love Netlify and all, but we use AWS for a lot of stuff at work, and it's just a bit more of an industry standard, so this is an effort to get more comfortable there. The landscape is full of deployment and hosting services - Netlify, Heroku, GitHub Pages, and of course Vercel, the turnkey solution for Next.js. Amplify is AWS's entry into this crowded space, tying together several distinct AWS frontend, backend, CI/CD, logging, and deployment services. Personally I'm a big fan of the GUI for environment variables, the deployment YAML file, and Git branch preview deployments. Of course, all of this can be managed through something like Terraform if you want to go the IaC route.

There's other stuff I'm toying with that I won't get into detail here - migrating DNS off of Google Domains, testing out Tailwind CSS, using Bun for local development. What I really wanted to write about in this blog post was running a Jest unit test in AWS Amplify, since it's not super straightforward how to run a simple little test.

The Test

I wrote this little <FormattedTime /> component, which accepts a date in YYYY-MM-DD format, and returns a readable date within a <time> element:

// app/components/FormattedDate.js
import { parseISO, format } from "date-fns";

export default function FormattedDate({ dateString }) {
  const date = parseISO(dateString);
  return (
    <time datetime={dateString}>
      {format(date, "LLLL d, yyyy")}
    </time>
  );
}

The idea is that it takes in a date format like 2010-02-04 and returns it as February 4, 2010, which is what gets rendered onto the page as the following:

<time datetime="2010-02-04">February 4, 2010</time>

Here's the test I wound up writing:

// tests/formattedDate.test.js
import FormattedDate from "../app/components/FormattedDate";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

describe("Date", () => {
  it("renders a date", () => {
    render(<FormattedDate dateString="2010-02-04" />);
    expect(screen.getByText("February 4, 2010"));
  });
});

As mentioned before, I wrote this test using Jest and React Testing Library. I can run the test using bun run test, which runs the following script (from my package.json):

// package.json
"test": "jest"

This command runs all of the tests in the tests directory. In the case of the formattedDate test, it's simply rendering the <FormattedDate /> component with the dateString prop value we pass it, simulating a browser DOM rendering that component, and checking for the text value "February 4, 2010" to be in that simulated browser DOM. Running it as-is, I get the following:

$ bun run test
$ jest
 PASS  tests/formattedDate.test.js
  Date
    ✓ renders a date (13 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.654 s, estimated 1 s

If I change the expected output:

// tests/formattedDate.test.js
import FormattedDate from "../app/components/FormattedDate";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

describe("Date", () => {
  it("renders a date", () => {
    render(<FormattedDate dateString="2010-02-04" />);
    expect(screen.getByText("December 4, 2010")); // changed the month here
  });
});

And rerun the test, then it (expectedly) fails:

$ bun run test
$ jest
 FAIL  tests/formattedDate.test.js
  Date
    ✕ renders a date (19 ms)

  ● Date › renders a date

    TestingLibraryElementError: Unable to find an element with the text: December 4, 2010. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.771 s, estimated 1 s

Pretty straightforward, huh?

Now I want to run this test through the AWS Amplify CI. But the problem is that the built-in testing area is only for End to End (E2E) testing.

The solve is to add npm run test to amplify.yml:

# amplify.yml
build:
  commands:
    - npm run test && npm run build

The useful thing here is to add it before the actual npm run build script. This way, if the unit test fails, it will stop the build and Amplify won't let you deploy until you revisit the test with a passing condition.