Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WRO-438: Update testing page with React Testing Lib #3049

Merged
merged 15 commits into from May 17, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 45 additions & 51 deletions docs/testing-components/test-driven-development/index.md
Expand Up @@ -9,9 +9,9 @@ the basic information on what unit testing is and how to start unit testing.

## Overview

This document describes the test-first methodology we aspire to on the Enact team. The concept behind Test Driven
Development (TDD) is that you write tests before you begin implementation. The theory is that testing helps you think
through the problem and serves as a sort of design for the forthcoming code. Additionally, the tests serve as a validation
This document describes the test-first methodology we aspire to on the Enact team. The concept behind Test Driven
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved
Development (TDD) is that you write tests before you begin implementation. The theory is that testing helps you think
through the problem and serves as a sort of design for the forthcoming code. Additionally, the tests serve as a validation
that refactoring has not broken the code.

## Introduction to TDD
Expand All @@ -27,27 +27,25 @@ Imagine we're going to create the `@enact/ui/IconButton` component and that the
are that it will have a `<Button>` containing an `<Icon>` as its children and that all properties assigned to the IconButton
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved
should be applied to the Button child except `minWidth`, which should always be `false`.

In this scenario, we would create an empty IconButton component that has no functionality. Then, we might write a test to
In this scenario, we would create an empty IconButton component that has no functionality. Then, we might write a test to
verify that an IconButton with `minWidth={true}` does not change the child component's property.

```js
describe('IconButton Specs', () => {

test('should always maintain minWidth=false for its <Button> child', () => {
const iconButton = mount(
<IconButton minWidth>star</IconButton>
);
const button = iconButton.find('Button');
const expected = false;
const actual = (button.prop('minWidth'));

expect(actual).toBe(expected);
test('should always have no minWidth class for its <Button> child', () => {
render(<IconButton minWidth>star</IconButton>);
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved
const button = screen.getByRole('button');

const expected = 'minWidth';

expect(button).not.toHaveClass(expected);
});
});
```

If we execute the test at this point it will fail. We have not implemented any functionality in our IconButton so we
should expect this will fail. TDD suggests we should write the minimal amount of code that will allow this test to pass.
If we execute the test at this point it will fail. We have not implemented any functionality in our IconButton, so we
should expect this will fail. TDD suggests we should write the minimal amount of code that will allow this test to pass.
So, we might write the following code in `IconButton.js`:

```js
Expand All @@ -60,65 +58,61 @@ const IconButton = () => {
};
```

This will make the test pass, but it's not a very useful IconButton. Let's add a test to check the requirement that other
This will make the test pass, but it's not a very useful IconButton. Let's add a test to check the requirement that other
properties are applied to the Button child.

```js
test('should apply same prop to <Button> child', function () {
const iconButton = mount(
test('should apply same class to <Button> child', function () {
render(
<IconButton size="small">star</IconButton>
);
const button = iconButton.find('Button');
const expected = true;
const actual = button.prop('small');

expect(actual).toBe(expected);
const button = screen.getByRole('button');
const expected = 'small';

expect(button).toHaveClass(expected);
});
```

When we run this test, it will fail. Now, we can wire up the property correctly and verify our component works. We can
When we run this test, it will fail. Now, we can wire up the property correctly and verify our component works. We can
then add a test for each new piece of functionality and then write the corresponding code to allow the test to pass.

While this process may seem a little naive, it does allow us to focus on writing the minimal amount of code that will solve
the problem at hand. It serves as a reminder of the YAGNI principle: You Ain't Gonna Need It. Don't overengineer the
the problem at hand. It serves as a reminder of the YAGNI principle: You Ain't Gonna Need It. Don't overengineer the
solution.

## Test Method Introduction

We use `Jest` for our unit testing. While there are quite a few comparisons it can help to stick to `.toBe()` and `.not.toBe()`. These methods come after
the `expect()` call.

We use Enzyme to render our components for testing. Enzyme can render a component in one of three different ways. Each
has its benefits and drawbacks. A summary of the methods follows.
We use React Testing Library to render our components for testing.
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved

The primary purpose of React Testing Library is to increase confidence in your tests by testing your components in the way a user would use them. Users don't care what happens behind the scenes, they just see and interact with the output. Instead of accessing the components' internal APIs or evaluating their state, you'll get more confidence by writing your tests based on the component output.

### Shallow Rendering - `shallow()`
### Component Rendering - `render()`

[Shallow](https://github.com/enzymejs/enzyme/blob/master/docs/api/shallow.md) rendering renders the component specified but does not render any of its children. This can be useful when you
only want to test the output of the single object. If you need to be able to test that properties get passed to children,
then you will need to use Mount rendering. Once a component is rendered a number of methods are available to inspect the
output. These include:
The render method works like this:

* `find()` - Returns nodes that match the passed-in selector. For custom components, usually you can use the name of the control
* `contains()` - Returns true if a node or array of nodes exist in the render
* `hasClass()` - Returns true if the component has the specified className
* `children()` - Returns the children of the component, wrapped so that these methods can be applied. (Note: In shallow render, the children will not be complete)
* `props()` - Returns the props of the component
* `prop()` - Returns the value of the specified prop
* `simulate()` - Simulates an event
* `instance()` - Returns the instance of the root component
* `setState()` - Sets the state of the component to the passed-in state
* `setProps()` - Manually sets the props of the component
```js
const {...results} = render(<Component {...props} />, {...options});
```

### Full Rendering - `mount()`
When we render the component we have a number of render options:

Full rendering or [mount](https://github.com/enzymejs/enzyme/blob/master/docs/api/mount.md) renders the component specified as well as all children. This can be useful when you need to be able to
test that properties get passed to children. Mount rendering uses the same methods as Shallow rendering listed above.
* `{container}` - By default, React Testing Library will create a div and append that div to the document.body and this is where your React component will be rendered. If you provide your own HTMLElement container via this option, it will not be appended to the document.body automatically.
* `{baseElement}` - If the container is specified, then this defaults to that, otherwise this defaults to document.body. This is used as the base element for the queries as well as what is printed when you use `debug()`.
* `{hydrate}` - If hydrate is set to true, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side rendering and use `ReactDOM.hydrate` to mount your components.
* `{legacyRoot}` - Used for apps that require rendering like in React 17 or older.
* `{wrapper}` - Pass a React Component as the wrapper option to have it rendered around the inner element. Returns the props of the component.
* `{queries}` - Queries to bind. Overrides the default set from DOM Testing Library unless merged.

### Static Rendering - `render()`
There are also some render results:

Static rendering or [render](https://github.com/enzymejs/enzyme/blob/master/docs/api/render.md), generates the HTML output from the specified component. Static rendering has several utility methods including:
* `{queries}` - The most important feature of render is that the queries from DOM Testing Library are automatically returned with their first argument bound to the baseElement, which defaults to document.body. See [Queries](https://testing-library.com/docs/queries/about/) for a complete list.
* `{container}` - The containing DOM node of your rendered React Element (rendered using ReactDOM.render).
* `{baseElement}` - If the container is specified, then this defaults to that, otherwise this defaults to document.body. This is used as the base element for the queries as well as what is printed when you use debug().
* `{debug}` - This method is a shortcut for console.log(prettyDOM(baseElement)).
* `{rerender}` - This function can be used to update props of the rendered component.
* `{unmount}` - This will cause the rendered component to be unmounted.
* `{asFragment}` - Returns a DocumentFragment of your rendered component.

* `text()` - Returns the text of the selected node
* `html()` - Returns the raw HTML of the selected node
* `children()` - Returns the children of the selected node
* `find()` - searches the node for the passed-in selector
80 changes: 33 additions & 47 deletions docs/testing-components/unit-testing/index.md
Expand Up @@ -34,7 +34,7 @@ component or item under test and end with the `"-specs.js"` suffix.
We use a dizzying number of tools to perform unit testing. A quick overview of the different tools can be helpful.

* [Jest](https://jestjs.io/) - A test framework. This tool allows us to setup, assert and run tests. We can also use `jest` as a mocking library.
* [Enzyme](https://enzymejs.github.io/enzyme/) - A test library for use with React. It allows us to shallowly render components and inspect the output.
* [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) - A test library for use with React. It allows us to render components and inspect the output.
* [jsdom](https://github.com/jsdom/jsdom) - A pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js.

## Unit Testing
Expand Down Expand Up @@ -115,41 +115,27 @@ and outputs we can test basically any JavaScript function that returns a value.

## Testing React

To test react we use [Enzyme](https://enzymejs.github.io/enzyme/) plus other tools you can find out about [here](../test-driven-development/index.md).
To test react we use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) plus other tools you can find out about [here](../test-driven-development/index.md).

```js
const Text = (props) => {
return <p>{props.content}</p>;
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved
}

test('Should contain text', () => {
const subject = shallow(
render(
<Text content='sample' />
);

const expected = 'sample';
const actual = subject.text()
expect(actual).toBe(expected);
const textElement = screen.querryByText('sample');
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved

expect(textElement).not.toBeNull();
});
```

If you wish to learn more about Enzyme's library of functions look [here](https://github.com/enzymejs/enzyme).

The three main parts about Enzyme that you need to know are it's rendering methods.

### shallow()

[Shallow](https://github.com/enzymejs/enzyme/blob/master/docs/api/shallow.md) is the virtual DOM representation. It will only render the component plus one level of children. This allows
us to stay within the smaller confines of a component when testing.

### mount()

[mount](https://github.com/enzymejs/enzyme/blob/master/docs/api/mount.md) is the virtual DOM representation. It will render everything inside the component, including all nested children.
This is a little beyond unit testing as you start to test the integration of a few components.
React Testing Library has access to a single rendering method.
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved

alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved
### render()

[render](https://github.com/enzymejs/enzyme/blob/master/docs/api/render.md) is the DOM representation. It will print a string of the output dom that the browser sees.
[render](https://testing-library.com/docs/react-testing-library/api#render) is the DOM representation. It will print a string of the output dom that the browser sees.

## Why Unit Testing?

Expand Down Expand Up @@ -180,7 +166,7 @@ as standard library functions or basic JavaScript behavior.

```js
// this is probably going to work
const returnArg (arg) => {
const returnArg = (arg) => {
return arg;
}

Expand All @@ -194,36 +180,37 @@ This example looks quite silly, but let's look at it in a React context:

```js
//original code
const Text = (props) => {
return <p>{props.content}</p>;
const Text = ({content, ...rest}) => {
return(<p {...rest}>{content}</p>);
}

//breaking change
const Text = (props) => {
return <p>{props.cont}</p>;
}
const Text = ({cont, ...rest}) => {
return(<p {...rest}>{cont}</p>);
};

//Example A - Bad
test('Should pass prop to component', () => {
const Text = shallow(
<Text content='sample' />
);

const expected = 'sample';
const contentProp = Text.prop('content')
expect(contentProp).toBe(expected);
render(
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved
<Text data-testid='text' content='sample' />
);

const textElement = screen.getByTestId('text');
const expected = 'sample';

expect(textElement).toHaveAttribute('content', expected);
});

//Example B - Better

test('Should contain text', () => {
const subject = shallow(
<Text content='sample' />
test('Should contain text', () => {
render(
<Text content='sample' />
);
const expected = 'sample';
const actual = subject.text()
expect(actual).toBe(expected);

const textElement = screen.querryByText('sample');
alexandrumorariu marked this conversation as resolved.
Show resolved Hide resolved

expect(textElement).toBeInTheDocument();
});
```

Expand All @@ -234,9 +221,8 @@ In test Example A, we can be very confident that React will do this correctly (p
break, even if somebody makes a change to the code. Example A will continue to pass because it tests passing arguments,
not what the component is supposed to display.

In test Example B, we have something that is fairly simple, but has a higher chance of breaking. We changed the property
that we're using to render. By testing the final output and not the property we get an accurate test. Also, this is
likely the only test we'd need for such a simple component.
In test Example B, we have something that is fairly simple, but has a higher chance of breaking. By testing the final
output and not the property we get an accurate test. Also, this is likely the only test we'd need for such a simple component.

## How Tests Influence Code

Expand All @@ -257,7 +243,7 @@ Simply stated, this means we can't have anything other than the arguments determ

```js
//Pure Function
const add2 (num) => {
const add2 = (num) => {
return num + 2
}
add2(4) //6
Expand Down Expand Up @@ -309,4 +295,4 @@ write tests in those cases.

Please refer to our document [Test Driven Development(TDD)](../test-driven-development/index.md), it contains how to run tests
and how they fit in our testing strategy. It also contains more information about TDD and the methodology behind it. This
document is a deeper dive into the unit test specifically.
document is a deeper dive into the unit test specifically.