###### tags: 今晚,就該來點前端測試 (P-2), frontend UT, test
# Note : Testing React with Jest and React Testing Library (RTL) (P-2)
現在就從最陽春的例子,把測試吃進肚子吧,因為對於 functional testing 而言,通常你會做一系列的操作 it's okay with multiple assertions in functional testing.
學習重點
1. TDD
2. interaction with virtaul DOM
3. assertion
4. Uint Test 要怎麼寫?與 funtional test 差異為何?
# ex
按鈕顏色為紅色,button 文字為 change to blue -> 按下按鈕 -> 顏色為藍色,文字為 change to red
(切版與畫面狀態略)
* step 1: render
* step 2: find the element ie using the global object screen that access to the virtual DOM created by the render
* step 3: testing our options in [jest DOM](https://github.com/testing-library/jest-dom)
```js
// App.js
function App() {
return (
<div>
<button style={{ background: "red" }}>Change to blue</button>
</div>
);
}
```
```js
// App.test.js
import { render, screen } from "@testing-library/react";
import App from "./App";
test("button has correct initial color", () => {
render(<App />);
const colorButton = screen.getByRole("button", { name: "Change to blue" });
expect(colorButton).toHaveStyle({
backgroundColor: "red",
});
});
```
Now, let interact with the DOM
fireEvent in @testing-library/react: interact with element in our virtual DOM
when one assertion fails, it won't continue to run the rest of the tests.
```js
// APP.test.js
test("button has correct initial color", () => {
render(<App />);
const colorButton = screen.getByRole("button", { name: "Change to blue" });
expect(colorButton).toHaveStyle({
backgroundColor: "red",
});
fireEvent.click(colorButton);
expect(colorButton).toHaveStyle({
backgroundColor: "blue",
});
expect(colorButton.textContent).toBe("Change to red");
});
```
現在,新增一個小功能,透過點 checkbox 可以讓原本的按鈕 disabled
```js
// App.test.js
test("button has initial state", () => {
render(<App />);
const colorButton = screen.getByRole("button", { name: "Change to blue" });
expect(colorButton).toHaveStyle({
backgroundColor: "red",
});
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeEnabled();
});
```
## QA: "why doesn't .toHaveStyle() work with classes from my imported CSS module?"
因為你的應用程式已經幫你把 css 模組 mock 起來,因為對絕大多數情境,css 是裝飾並不影響你的功能. 但有些情況是會影響功能,像是 `dispaly: none`;
為了讓樣式可以被測試解讀,需要做一些轉換
* https://www.npmjs.com/package/jest-transform-css
* https://www.npmjs.com/package/jest-css-modules-transform
## HW
要求:前端要自己寫不要 copy paste
題目:when checkbox is checked, button should be disabled
Hint:
1. 2x in test: once to disabled, once to re-ebabled
2. fireEvent.click
3. onChange for checkbox; checkbox controls via a boolean
```js
function App() {
const [buttonColor, setButtonColor] = useState("red");
const [disabled, setDisabled] = useState(false);
const newButtonColor = buttonColor === "red" ? "blue" : "red";
return (
<div>
<button
style={{ backgroundColor: buttonColor }}
onClick={() => setButtonColor(newButtonColor)}
disabled={disabled}
>
Change to {newButtonColor}
</button>
<input type="checkbox" onClick={(e) => setDisabled(e.target.checked)} />
</div>
);
}
```
```js
test("checkbox disabled button on first click and enabled on second click", () => {
render(<App />);
const colorButton = screen.getByRole("button", { name: "Change to blue" });
expect(colorButton).toBeEnabled();
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(colorButton).toBeDisabled();
fireEvent.click(checkbox);
expect(colorButton).toBeEnabled();
});
```
上述的選定 checkbox 有個小問題,如果同時有多個 checkobx 要怎麼辦?
```js
// 透過指定 name
const checkbox = screen.getByRole("checkbox", { name: "Disabled Button" });
// TestingLibraryElementError: Unable to find an accessible element with the role "checkbox" and name "Disabled Button"
```
checkbox has no name, so let add a label to remedy that.
```js
<input
type="checkbox"
onClick={(e) => setDisabled(e.target.checked)}
id="disabled-button-checkbox"
/>
<label htmlFor="disabled-button-checkbox">Disabled Button</label>
```
題目:the button turns gray when it's disabled
Test flow (simulate possible user flows)
1. disabled button -> button is gray -> enabled button -> button is red
2. click button -> button is blue -> disabled button -> button is gray -> enabled button -> button is blue
make two tests due to two different flows
```js
test("button turns gray when it's disabled - disabled button first", () => {
render(<App />);
const colorButton = screen.getByRole("button", { name: "Change to blue" });
const checkbox = screen.getByRole("checkbox", { name: "Disabled Button" });
fireEvent.click(checkbox);
expect(colorButton).toHaveStyle({ backgroundColor: "gray" });
fireEvent.click(checkbox);
expect(colorButton).toHaveStyle({ backgroundColor: "red" });
});
test("button turns gray when it's disabled - click button first", () => {
render(<App />);
const colorButton = screen.getByRole("button", { name: "Change to blue" });
const checkbox = screen.getByRole("checkbox", { name: "Disabled Button" });
fireEvent.click(colorButton);
expect(colorButton).toHaveStyle({ backgroundColor: "blue" });
fireEvent.click(checkbox);
expect(colorButton).toHaveStyle({ backgroundColor: "gray" });
fireEvent.click(checkbox);
expect(colorButton).toHaveStyle({ backgroundColor: "blue" });
});
```
# unit testing
function seperated from component
1. used by several component
2. complex logic
UT if
1. too many edge cases
2. complex logic difficult to test via functional tests
3. determining what caused functional tests to fail
## ex
```js
export const replaceCamelWithSpace = (colorName) => {
return colorName.replace(/\B([A-Z])\B/g, " $1");
};
```
```js
// describe statement is a way to group by tests.
describe("spaces before camel-case capital letters", () => {
test("works for no inner capital letter", () => {
expect(replaceCamelWithSpace("Red")).toBe("Red");
});
test("works for one inner capital letter", () => {
expect(replaceCamelWithSpace("MidnightBlue")).toBe("Midnight Blue");
});
test("works for multiple inner capital letters", () => {
expect(replaceCamelWithSpace("MediumVioletRed")).toBe("Medium Violet Red");
});
});
```
## hw
題目:color starts with MediumVioletRed and changes to MidnightBlue
(略)
# Issue with functional tests
1. high-level makes them resistent to refactor :+1:
2. high-level makes them hard to diagonse :-1: