Initial Due Date: 2025-03-06 9:45AM
Final Due Date: 2025-03-27 4:15PM
đź’» git clone
(get the name of the repository from GitHub).package.json
file and add your name as the author of the package.đź’» pnpm install
in the terminal.You previously used Jest for unit testing JS code. Today we are going to use Jest in combination with the Testing Library library to test a React application. There are a number of items that need to be installed, but the project skeleton includes everything you need.
The easiest form of testing we can perform is called a smoke test. Unlike the testing we saw earlier, we aren’t going to take any actions or assert anything. All we will do is try to render a component. If the process throws any errors, the test will fail, otherwise it succeeds. This kind of test is great for quick and dirty regression testing, where we are trying to make sure that adding new features or fixing bugs hasn’t inadvertently introduced any errors. Note that it doesn’t actually tell you that the functionality hasn’t been broken, just that it didn’t catch fire as it were (the name comes from the hardware development, where a smoke test means “plug it in and see if smoke comes out”).
For our smoke test, we will render the entire App
component. You will find the file distance.test.js in the src/__tests__ directory. The skeleton provides the imports for you. All you need to do is render the component; add the following to create your smoke test:
test("Smoke test", () => {
render(<App />);
});
Jest provides another quick regression testing tool, snapshots. You take a snapshot of the component at a time when you like the way it looks. Jest saves a representation of the component, and then every time you run the tests, Jest regenerates the component and compares it to the snapshot. If the component changes, the test will fail, which is a cue to either fix the offending code, or to take a new snapshot because the change was intentional. Note that the snapshot is not a literal picture, it is a textual description of the component that can be quickly compared. Add the following to distance.test.js to create the snapshot:
test("Snapshot test", () => {
const { asFragment } = render(<App />);
expect(asFragment()).toMatchSnapshot();
});
Here we are getting the asFragment
function from the render
function. The asFragment
function returns a DocumentFragment
with the rendered component. We then snapshot that fragment.
Note that we didn’t write anything to generate the snapshot. Jest will do that automatically the first time the test is run. Go ahead and run the tests. You will find that Jest has created a new directory called __snapshots__ in the same directory as your test file. Open this up and look at the snapshot that is stored in there. This should be committed with your code so that subsequent tests can use it.
So that you can see how the snapshot test works, go into src/pages/index.js and find where the place where write the name of the app (“Distance Converter”) and change it to something else. If you run the test now, the snapshot test will fail. Notice that you are provided with a diff showing what has changed. Of course, sometimes you will be making a change and you want the page to be different. You can update your snapshot with 💻 pnpm test -u
(or if you are running the test watcher, đź’» pnpm test --watch
, you can just type u). Update your snapshot to acknowledge your change.
Can you use snapshots for TDD? Peek at the answer.
No. Recall in TDD we write the tests first. But snapshots require a working implementation. Snapshots are most useful for regression testing.
DistanceUnit
The DistanceConverter
is composed of DistanceUnit
s. Let’s start our testing there. Here is an initial “Arrange” step, where we render the DistanceUnit
component with a mock function for the callback. The actions involve the numeric input, so we need to obtain that element using RTL’s query tools. We can use the website https://testing-playground.com to help us figure out good queries (amongst the many possibilities).
screen.debug()
call prints the generated DOM to the console. Copy the printed DOM into https://testing-playground.com.getByRole
uses the ARIA role for the number input, made more precise by matching (with a regular expression for a case-insensitive substring match) the name of the associated label.
screen.getByRole('spinbutton', { name: /miles/i });
describe("DistanceUnit", () => {
test("DistanceUnit invokes callback with converted input", () => {
// Arrange
const setMetersMock = jest.fn();
render(
<DistanceUnit
label="miles"
valueInMeters={0}
setMeters={setMetersMock}
conversionFactor={0.000621371}
/>,
);
screen.debug(); // Render DOM to console
});
});
Now, remove screen.debug()
and insert the query and the “arrange” assertion on the initial value:
const input = screen.getByRole("spinbutton", { name: /miles/i });
expect(input).toHaveValue(0);
The “action” is to enter a distance. Add the following fireEvent
call to update the input value.
fireEvent.change(input, { target: { value: 1 } });
Consider the assertions. Note that the input in DistanceUnit
is a controlled component (controlled by the valueInMeters
prop). Thus the action won’t actually change the value. Instead it will trigger the callback. Add assertions that the value remains the same and the callback is invoked with the expected value (based on the conversion factor for miles to meters). For example:
expect(input).toHaveValue(0);
expect(setMetersMock).toHaveBeenCalledWith(
expect.closeTo(1 / 0.000621371, 2),
);
Comparing floating point values (i.e., fractional numbers) for equality is fraught. Differences in rounding can lead to slightly different values that compare as not equal but are effectively the same. As a result we want to compare floating points numbers within some error range. Here we use expect.closeTo(number, numDigits?))
, a Jest matcher for floating point values. It requires the input to be equal within numDigits
after the decimal point.
Run the tests to verify they pass. Temporarily change the miles conversion factor in the component (i.e., break the component) to verifying that the tests are actually catching errors.
DistanceConverter
We have a general pattern that we follow when writing tests around state changes.
The first step is often overlooked, but important to establish that the system is “Arranged” in a known baseline state. Without that, we can’t know that a state change actually occurred. The last two steps are less important, but worth doing when the state is a form of binary toggle (less relevant here).
In the existing test for DistanceConverter
add assertions to verify the initial state of the component. In this case, we expect all the units to have a value of 0 (based on the initial value of the meters
state). Recall that we can select specific inputs by specifying the relevant “name” for the number input. Make sure you are testing all the different units (think about how you could use forEach
to do so concisely). As a suggestion consider making an array of objects with the expected labels and conversion factors that you can use to implement your tests. For example:
const units = [
{ label: "meters", fromMeters: 1 },
{ label: "miles", fromMeters: 0.000621371 },
]
Above we used a hard-coded regular expression literal (/meters/i
) to match the button by label. But now we want to match based on a variable, e.g., label
. To do so, we can create the regular expression dynamically. For example, assuming label
is assigned "meters"
, the following
screen.getByRole("spinbutton", {
name: new RegExp(label, "i"),
});
is equivalent to screen.getByRole("spinbutton", { name: /miles/i })
.
The skeleton includes an “Act” step that updates the “meters” input to be 1. Add a corresponding set of assertions to verify the new state. One such assertion is included in the skeleton. Notice we extract the current value of the component, which is a string, convert it to a Number
and compare the expected value within 2 decimal places.
Recall that in TDD, we first write the tests then the code. We want to add inches as a unit to the converter component (i.e., we should have miles, km, feet, meters and inches). Start by extending your tests to include an “inches” input. At this point the tests should fail since you have’t yet implemented the necessary code. Now extend the DistanceConverter
component (in src/pages/index.js) to includes inches. At this point your tests should all pass!
đź’» pnpm test
) and there are no linting errors (with đź’» pnpm run lint
)If you attempt to push your changes to GitHub and get an error like the following, your repository on GitHub has commits that you local clone does not (as a result of how GitHub classroom seems to work, see details below). You will need to pull the changes you don’t have. In this case, you can safely and effectively use rebase to do so, i.e., execute 💻 git pull --rebase origin main
in the terminal. Then attempt to push again.
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:csci312a-s25/assignment01...'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
It appears that there can sometimes be a delay between when GitHub classroom creates your repository and when it finishes adding its automatic commits (for the due date, etc.). Thus is it possible (easy) to clone the repository before that process has completed and end up in a situation where the GitHub repository has commits your local clone does not.
Required functionality:
DistanceUnit
testsDistanceConverter
testsDistanceConverter
includes inches fieldRecall that the Practical exercises are evaluated as “Satisfactory/Not yet satisfactory”. Your submission will need to implement all of the required functionality (i.e., pass all the tests) to be Satisfactory (2 points).