This commit is contained in:
Ken 2019-03-02 20:57:25 -08:00
Родитель 8fc928ea8d
Коммит b91914e1d8
21 изменённых файлов: 296 добавлений и 221 удалений

Просмотреть файл

@ -0,0 +1,128 @@
# Step 2.4: Testing TypeScript code with Jest (Demo)
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
[Jest](https://jestjs.io/) is a test framework made by Facebook and is very popular in the React and wider JS ecosystems.
In this exercise, we will work on implementing simple unit tests using Jest.
## Jest Features
- Multi-threaded and isolated test runner
- Provides a fake browser-like environment if needed (window, document, DOM, etc) using jsdom
- Snapshots: Jest can create text-based snapshots of rendered components. These snapshots can be checked in and show API or large object changes alongside code changes in pull requests.
- Code coverage is integrated (`--coverage`)
- Very clear error messages showing where a test failure occurred
## How to use Jest
- Using `create-react-app` or other project generators, Jest should already be pre-configured. Running `npm test` usually will trigger it!
- A `jest.config.js` file is used for configuration
- `jsdom` might not have enough API from real browsers, for those cases, polyfills are required. Place these inside `jest.setup.js` and hook up the setup file in `jest.config.js`
- in order to use `enzyme` library to test React Components, more config bits are needed inside `jest.setup.js`
## What does a test look like?
```ts
// describe(), it() and expect() are globally exported, so they don't need to be imported when jest runs these tests
describe('Something to be tested', () => {
it('should describe the behavior', () => {
expect(true).toBe(true);
});
});
```
## Testing React components using Enzyme
[Enzyme](https://airbnb.io/enzyme/) is made by Airbnb and provides utilities to help test React components.
In a real app using ReactDOM, the top-level component will be rendered on the page using `ReactDOM.render()`. Enzyme provides a lighter-weight `mount()` function which is usually adequate for testing purposes.
`mount()` returns a wrapper that can be inspected and provides functionality like `find()`, simulating clicks, etc.
The following code demonstrates how Enzyme can be used to help test React components.
```jsx
import React from 'react';
import { mount } from 'enzyme';
import { TestMe } from './TestMe';
describe('TestMe Component', () => {
it('should have a non-clickable component when the original InnerMe is clicked', () => {
const wrapper = mount(<TestMe name="world" />);
wrapper.find('#innerMe').simulate('click');
expect(wrapper.find('#innerMe').text()).toBe('Clicked');
});
});
describe('Foo Component Tests', () => {
it('allows us to set props', () => {
const wrapper = mount(<Foo bar="baz" />);
expect(wrapper.props().bar).toBe('baz');
wrapper.setProps({ bar: 'foo' });
expect(wrapper.props().bar).toBe('foo');
wrapper.find('button').simulate('click');
});
});
```
## Advanced topics
### Mocking
Mocking functions is a large part of what makes Jest a powerful testing library. Jest actually intercepts the module loading process in Node.js, allowing it to mock entire modules if needed.
There are many ways to mock, as you'd imagine in a language as flexible as JS. We only look at the simplest case, but there's a lot of depth here.
To mock a function:
```ts
it('some test function', () => {
const mockCallback = jest.fn(x => 42 + x);
mockCallback(1);
mockCallback(2);
expect(mockCallback).toHaveBeenCalledTimes(2);
});
```
Read more about jest mocking [here](https://jestjs.io/docs/en/mock-functions.html).
### Async Testing
For testing async scenarios, the test runner needs some way to know when the scenario is finished. Jest tests can handle async scenarios using callbacks, promises, or async/await.
```ts
// Callback
it('tests callback functions', (done) => {
setTimeout(() => {
done();
}, 1000);
});
// Returning a promise
it('tests promise functions', () => {
return someFunctionThatReturnsPromise());
});
// Async/await (recommended)
it('tests async functions', async () => {
expect(await someFunction()).toBe(5);
});
```
# Demo
## Jest basics
In this repo, we can start an inner loop development of tests by running `npm test` from the root of the `frontend-bootcamp` folder.
Take a look at code inside `demo/src`:
1. `index.ts` exports a few functions for a counter as well as a function for squaring numbers. We'll use this last function to demonstrate how mocks work.
2. `multiply.ts` is a contrived example of a function that is exported
3. `index.spec.ts` is the test file
Note how tests are re-run when either test files or source files under `src` are saved.

Просмотреть файл

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../../assets/step.css" />
</head>
<body class="ms-Fabric">
<div id="markdownReadme" data-src="./README.md"></div>
<div id="app">
For this step, we look at unit testing. Run
<pre>npm test</pre>
in the command line.
</div>
<script src="../../assets/scripts.js"></script>
</body>
</html>

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Двоичные данные
assets/flux.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 27 KiB

После

Ширина:  |  Высота:  |  Размер: 50 KiB

Двоичные данные
assets/todo-components.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 54 KiB

Просмотреть файл

@ -1,53 +0,0 @@
import marked, { Renderer } from 'marked';
import hljs from 'highlight.js/lib/highlight';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
async function run() {
const div = document.getElementById('markdownReadme');
// Create your custom renderer.
const renderer = new Renderer();
renderer.code = (code, language) => {
// Check whether the given language is valid for highlight.js.
const validLang = !!(language && hljs.getLanguage(language));
// Highlight only if the language is valid.
const highlighted = validLang ? hljs.highlight(language, code).value : code;
// Render the highlighted code with `hljs` class.
return `<pre><code class="hljs ${language}">${highlighted}</code></pre>`;
};
marked.setOptions({ renderer });
if (div) {
const response = await fetch(div.dataset['src'] || '../README.md');
const markdownText = await response.text();
div.innerHTML = marked(markdownText, { baseUrl: '../' });
restoreScroll(div);
div.addEventListener('scroll', evt => {
saveScroll(div);
});
window.addEventListener('resize', evt => {
saveScroll(div);
});
}
}
const scrollKey = `${window.location.pathname}_scrolltop`;
function saveScroll(div) {
window.localStorage.setItem(scrollKey, String(div.scrollTop));
}
function restoreScroll(div) {
const scrollTop = window.localStorage.getItem(scrollKey);
if (scrollTop) {
div.scrollTop = parseInt(scrollTop);
}
}
run();

Просмотреть файл

@ -22,7 +22,7 @@ export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState
return (
<Stack gap={10}>
<Stack horizontal horizontalAlign="center">
<Text variant="xxLarge">todos - step2-03 demo</Text>
<Text variant="xxLarge">todos</Text>
</Stack>
<Stack horizontal gap={10}>
@ -40,9 +40,7 @@ export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState
})}
/>
</Stack.Item>
<PrimaryButton onClick={this.onAdd} styles={{ root: { backgroundColor: 'maroon' }, rootHovered: { background: 'green' } }}>
Add
</PrimaryButton>
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
</Stack>
<Pivot onLinkClick={this.onFilter}>

Просмотреть файл

@ -1,128 +1,108 @@
# Step 2.4: Testing TypeScript code with Jest (Demo)
# Step 2.4 - React Context (Demo)
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
[Jest](https://jestjs.io/) is a test framework made by Facebook and is very popular in the React and wider JS ecosystems.
In this step, we describe some problems we encounter when creating a more complex application.
In this exercise, we will work on implementing simple unit tests using Jest.
We will solve these problems with the React Context API. The Context API consists of:
## Jest Features
1. Provider component
2. Consuming context from a Class Component
3. Consuming context from a Functional Component
- Multi-threaded and isolated test runner
- Provides a fake browser-like environment if needed (window, document, DOM, etc) using jsdom
- Snapshots: Jest can create text-based snapshots of rendered components. These snapshots can be checked in and show API or large object changes alongside code changes in pull requests.
- Code coverage is integrated (`--coverage`)
- Very clear error messages showing where a test failure occurred
---
## How to use Jest
For a single component, React gives us a mental model like this:
- Using `create-react-app` or other project generators, Jest should already be pre-configured. Running `npm test` usually will trigger it!
- A `jest.config.js` file is used for configuration
- `jsdom` might not have enough API from real browsers, for those cases, polyfills are required. Place these inside `jest.setup.js` and hook up the setup file in `jest.config.js`
- in order to use `enzyme` library to test React Components, more config bits are needed inside `jest.setup.js`
```
(props) => view;
```
## What does a test look like?
In a real application, these functions are composed. It looks more like this:
![](../../assets/todo-components.png)
## Problems in a Complex Application
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data.
2. There is a lack of coordination of changes that can happen to the data
Even in our simple application, we saw this problem. For example, `<TodoList>` has this props interface:
```ts
// describe(), it() and expect() are globally exported, so they don't need to be imported when jest runs these tests
describe('Something to be tested', () => {
it('should describe the behavior', () => {
expect(true).toBe(true);
});
});
interface TodoListProps {
complete: (id: string) => void;
remove: (id: string) => void;
todos: Store['todos'];
filter: FilterTypes;
edit: (id: string, label: string) => void;
}
```
## Testing React components using Enzyme
All of these props are not used, except to be passed down to a child Component, `TodoListItem`:
[Enzyme](https://airbnb.io/enzyme/) is made by Airbnb and provides utilities to help test React components.
In a real app using ReactDOM, the top-level component will be rendered on the page using `ReactDOM.render()`. Enzyme provides a lighter-weight `mount()` function which is usually adequate for testing purposes.
`mount()` returns a wrapper that can be inspected and provides functionality like `find()`, simulating clicks, etc.
The following code demonstrates how Enzyme can be used to help test React components.
```jsx
import React from 'react';
import { mount } from 'enzyme';
import { TestMe } from './TestMe';
describe('TestMe Component', () => {
it('should have a non-clickable component when the original InnerMe is clicked', () => {
const wrapper = mount(<TestMe name="world" />);
wrapper.find('#innerMe').simulate('click');
expect(wrapper.find('#innerMe').text()).toBe('Clicked');
});
});
describe('Foo Component Tests', () => {
it('allows us to set props', () => {
const wrapper = mount(<Foo bar="baz" />);
expect(wrapper.props().bar).toBe('baz');
wrapper.setProps({ bar: 'foo' });
expect(wrapper.props().bar).toBe('foo');
wrapper.find('button').simulate('click');
});
});
```js
<TodoListItem todos="{todos}" complete="{complete}" remove="{remove}" edit="{edit}" />
```
## Advanced topics
## Context API
### Mocking
Let's solve the first one with the Context API. A `context` is a special way for React to share data from components to their descendant children components without having to explicitly pass down through props at every level of the tree.
Mocking functions is a large part of what makes Jest a powerful testing library. Jest actually intercepts the module loading process in Node.js, allowing it to mock entire modules if needed.
There are many ways to mock, as you'd imagine in a language as flexible as JS. We only look at the simplest case, but there's a lot of depth here.
To mock a function:
We create a context by calling `createContext()` with some initial data:
```ts
it('some test function', () => {
const mockCallback = jest.fn(x => 42 + x);
mockCallback(1);
mockCallback(2);
expect(mockCallback).toHaveBeenCalledTimes(2);
});
const TodoContext = React.createContext();
```
Read more about jest mocking [here](https://jestjs.io/docs/en/mock-functions.html).
Now that we have a `TodoContext` stuffed with some initial state, we will wrap `TodoApp` component with `TodoContext.Provider` so that it can provide data to all its children:
### Async Testing
For testing async scenarios, the test runner needs some way to know when the scenario is finished. Jest tests can handle async scenarios using callbacks, promises, or async/await.
```ts
// Callback
it('tests callback functions', (done) => {
setTimeout(() => {
done();
}, 1000);
});
// Returning a promise
it('tests promise functions', () => {
return someFunctionThatReturnsPromise());
});
// Async/await (recommended)
it('tests async functions', async () => {
expect(await someFunction()).toBe(5);
});
```js
class TodoApp extends React.Component {
render() {
return (
<TodoContext.Provider
value={{
...this.state,
addTodo={this._addTodo},
setFilter={this._setFilter},
/* same goes for remove, complete, and clear */
}}>
<div>
<TodoHeader />
<TodoList />
<TodoFooter />
</div>
</TodoContext.Provider>
);
}
}
```
# Demo
Inside the children components, like the `<TodoHeader>` component, the value can be access from the component's `context` prop like this:
## Jest basics
```js
class TodoHeader extends React.Component {
render() {
// Step 1: use the context prop
return <div>Filter is {this.context.filter}</div>;
}
}
In this repo, we can start an inner loop development of tests by running `npm test` from the root of the `frontend-bootcamp` folder.
// Step 2: be sure to set the contextType property of the component class
TodoHeader.contextType = TodoContext;
```
Take a look at code inside `demo/src`:
If you're using the functional component syntax, you can access the context with the `useContext()` function (we are using the function passed down inside the context, in this case):
1. `index.ts` exports a few functions for a counter as well as a function for squaring numbers. We'll use this last function to demonstrate how mocks work.
2. `multiply.ts` is a contrived example of a function that is exported
3. `index.spec.ts` is the test file
Note how tests are re-run when either test files or source files under `src` are saved.
```js
const TodoFooter = props => {
const context = useContext(TodoContext);
return (
<div>
<button onClick={context.clear()}>Clear Completed</button>
</div>
);
};
```

Просмотреть файл

@ -5,11 +5,7 @@
</head>
<body class="ms-Fabric">
<div id="markdownReadme" data-src="./README.md"></div>
<div id="app">
For this step, we look at unit testing. Run
<pre>npm test</pre>
in the command line.
</div>
<div id="app"></div>
<script src="../../assets/scripts.js"></script>
</body>
</html>

Просмотреть файл

@ -1,101 +1,112 @@
# Step 2.5 - Redux: Reducers (Demo)
# Step 2.5 - Redux: The Store (Demo)
[Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/)
## Flux
In this step, we will look at solving the problems of complex application (as mentioned in Step 4) with a library called Redux.
Ideally React gives us a mental model of:
1. Introduction to Redux
2. Creating the Redux store
3. Writing reducers
4. Dispatching actions
```
f(data) => view
```
---
We can pass shared state data down to components using props and update it using callbacks, but as an application grows larger, passing around callbacks becomes unwieldy and the state data flow becomes hard to understand. To keep the application maintainable, we need a well-defined pattern for sharing and updating application state.
As a reminder, the problem that we want to address are:
Facebook invented the [Flux](https://facebook.github.io/flux/) architectural pattern to solve this shared state issue. [Redux](https://redux.js.org/) is an implementation of Flux.
1. Data needs to be passed down from component to component via props. Even when some components do not need to know about some data.
2. There is a lack of coordination of changes that can happen to the data
Redux is used inside many large, complex applications because of its clarity and predictability. It is easy to debug and easily extensible via its middleware architecture. In this lesson, we'll explore the heart of how Redux manages state.
Redux expects the data (store) to be a singleton state tree. It listens for messages to manipulate the state and passes updates down to views.
Redux is an implementation of the Flux architectural pattern:
![Flux Diagram](../assets/flux.png)
### View
A view is a React component that consumes the store as its data. There is a special way Redux maps data from the state tree into the different React components. The components will know to re-render when these bits of state are changed.
A view is a React component that consumes the store as its data.
### Action
[Actions](https://redux.js.org/basics/actions) are messages that represent some event, such as a user's action or a network request. With the aid of *reducers*, they affect the overall state.
[Actions](https://redux.js.org/basics/actions) are messages that represent some event, such as a user's action or a network request. With the aid of _reducers_, they affect the overall state.
### Store
The [store](https://redux.js.org/basics/store) contains a singleton state tree. The state tree is immutable and needs to be re-created at every action. This helps connected views easily to know when to update by allowing them to do a simple reference comparison rather than a deep comparison. You can think of each state as a snapshot of the app at that point in time.
The [store](https://redux.js.org/basics/store) consists of a **state tree**, a **dispatcher**, and **reducers**.
### Dispatcher
1. A state tree is a **singleton**, **serializable**, **immutable** json data. It is updated from one snapshot to another through `reducers`.
There is a single [dispatcher](https://redux.js.org/basics/data-flow), provided by the store. It simply informs the store of all the actions that need to be performed. Additional middleware (explained later) can be applied to the store, and the dispatcher's job is to dispatch the message through all the middleware layers.
2. A dispatcher accepts actions passing them to the reducers.
### Reducers
3. Reducers are functions that take in the current state tree and an action, producing the next snapshot of the state tree.
Redux uses [reducers](https://redux.js.org/basics/reducers) to manage state changes. This name comes from the "reducer" function passed to `Array.reduce()`.
A Redux reducer is a simple **pure function** (no side effects). Its only job is to transform the state from one immutable snapshot to another. It takes state + an action message as input, makes a modified copy of the state based on the action message type and payload, and returns the new state. (Reducers [should not modify](https://redux.js.org/introduction/three-principles#state-is-read-only) the previous state.)
**Mental Model**: Think of a reducer as part of the store. It should have no side effects and only define how data changes from one state to the next given action messages.
### Advanced: Middleware
From the [documentation site](https://redux.js.org/advanced/middleware):
> Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
We won't be covering middleware much in these lessons since it's a more advanced topic.
## Getting started with Redux
We begin the journey into Redux by looking at the store. The store consists of several parts:
1. **State/data** - We represent this both with an initial state and with a TypeScript interface.
2. **Reducers** - Responsible for reacting to action messages to change from one state to the next.
3. **Dispatcher** - There should be only one dispatcher, which is exported by the store. We'll look at this in Step 6!
### Create store
# Creating the Redux store
The [`createStore()`](https://redux.js.org/api/createstore) function is provided by Redux and can take in several arguments. The simplest form just takes in reducers.
```ts
const store = createStore(reducer);
const store = createStore(reducer, initialState);
```
`createStore()` can also take in an initial state. There are two common sources for the initial state:
`createStore()` creates a store with a reducer, and some initial state.
1. Load initial data from a server
2. Load data that is generated by a server-side rendering environment
# Writing Reducers
We will write our reducers with the help of some utilities from `redux-starter-kit`. Here is how we will write our reducers:
## 1. Organize reducers according to the keys of the state tree object:
```ts
const store = createStore(reducer, {
/* the initial state */
import { createReducer } from 'redux-starter-kit';
const todosReducer = createReducer({}, {
// first argument is the initial state
// second argument is an object where the keys corresponds to the "action.type"
addTodo: (state, action) => ...
});
const filterReducer = createReducer('all', {
setFilter: (state, action) => ...
});
const reducer = combineReducer({
todos: todosReducer,
filter: filterReducer
})
```
`createStore()` can take a third argument that injects middleware, but we won't use this until later.
## 2. Write the reducers with mutables.
### Reducers
Remember that the [reducers are **pure**](https://redux.js.org/introduction/three-principles#changes-are-made-with-pure-functions). Pure functions have no side effects. They always return the same output given the same input (idempotent). They are easily testable.
Reducers look at the action's message to decide what to do to the state. A convention established in the Flux community is that the action message (payload) should include a `type` key. Another convention is using switch statements against the `type` key to trigger further reducer functions.
`createReducer()` will automatically translate all the mutations to the state into immutable snapshots (!!!!!):
```ts
function reducer(state: Store['todos'], payload: any): Store['todos'] {
switch (payload.type) {
case 'addTodo':
return addTodo(state, payload.id, payload.label);
const todosReducer = createReducer(
{},
{
// first argument is the initial state
// second argument is an object where the keys corresponds to the "action.type"
addTodo: (state, action) => {
state[action.id] = { label: action.label, completed: false };
}
}
return state;
}
);
```
In the demo and exercises for this step, I separated the pure and reducer functions into different files to make it cleaner. The tests inside `pureFunctions.spec.ts` should describe the behavior of the individual functions. They are easy to follow and easy to write.
# Dispatching Actions
Dispatching action will pass the action and the current state to the _reducers_. The root _reducer_ will produce a new snapshot for the entire state tree. We can inspect the affected snapshot with the help of `getState()`.
```ts
const store = createStore(reducer, initialState);
store.dispatch({ type: 'addTodo', label: 'hello' });
store.dispatch({ type: 'addTodo', label: 'world' });
console.log(store.getState());
```
Creating these action messages by hand is tedious, so we use action creators to do that:
```ts
const actions = {
addTodo = (label: string) => ({ label, id: nextId(), completed: false })
};
store.dispatch(actions.addTodo('hello'));
```