30天也剩沒幾天了,現在才要進入 devops?!
圖片來源:Docker (@Docker) / Twitter
前面基礎建設都弄好了
接下來就以簡易的前後端來實際跑一次流程~
因為沒什麼時間可以弄網站,所以就拿個 TodoList 來弄好了~
以 React Redux 當底玩看看~
完整專案在 github Day 24 - frontend
簡單列一下套件~
package.json
{
...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --progress --color --config webpack.dev.js --open",
"start": "webpack-dev-server --open --progress --config webpack.dev.js",
"test": "jest"
},
"dependencies": {
"@mui/material": "^5.5.3",
"react": "^17.0.2",
"redux": "^4.1.2",
"redux-saga": "^1.1.3"
...
},
"devDependencies": {
"@babel/preset-env": "^7.19.3",
"@babel/preset-react": "^7.18.6",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3",
"babel-jest": "^29.1.2",
"jest": "^29.1.2",
"jest-environment-jsdom": "^29.1.2",
"react-test-renderer": "^17.0.2",
"stylus": "^0.54.8",
...
},
"jest": {
"testEnvironment": "jsdom"
}
}
既然都要 devops 了,就跑一點測試吧~
目前寫測試的能力等級還在新手菜鳥,只能簡單寫一下 component 的 unit test,請多見諒~ ??
功能大概是從 api 取得 TodoList (get_todolist
),在畫面上操作新增、刪除,完成後送出完成修改 (put_todolist
)
export default function TodoList(props) {
const {
data, isFetching, errmsg,
get_todolist,
put_todolist,
} = props
const [Todos, setTodos] = useState([])
useEffect(() => {
get_todolist()
}, [])
useEffect(() => {
if (!!data) {
setTodos(data)
}
return () => {
setTodos([])
}
}, [data])
const AddItem = (item) => {
setTodos([item, ...Todos])
}
const DeleteItem = (i) => {
const items = Array.from(Todos)
items.splice(i, 1)
setTodos(items)
}
const onSubmit = (data) => {
put_todolist({ data })
}
return (
<Container maxWidth="md">
<Card>
<CardHeader
title={<Typography variant="body1">TODO</Typography>}
subheader={<Divider />} />
{
isFetching ?
<CardContent>
<Loading />
</CardContent>
: <Content
Todos={Todos}
AddItem={AddItem}
DeleteItem={DeleteItem}
onSubmit={onSubmit} />
}
</Card>
</Container>
)
}
接下來依照功能跟著列出測試方法~
輸入項目名稱 (TextField) 之後按下新增 (Button)
export function AddListItem(props) {
const { AddItem } = props
const [input, setInput] = useState("")
const onChange = (event) => {
let value = event.target.value
setInput(value)
}
const handleonClick = (event) => {
setInput("")
AddItem(input)
}
return (
<Grid container spacing={1} alignItems="center">
<Grid item xs={8}>
<TextField label="Item-Name" value={input} onChange={onChange} fullWidth variant="standard" />
</Grid>
<Grid item>
<Button onClick={handleonClick} variant="contained">新增</Button>
</Grid>
</Grid>
)
}
輸入項目名稱後,點擊"新增"按鈕,會回傳輸入值給 AddItem
test('輸入 Item Name 及新增按鈕', () => {
const AddItem = jest.fn()
const utils = render(<AddListItem AddItem={AddItem} />)
const itemInput = utils.getByLabelText('Item-Name')
const text = "Item-Text-1"
fireEvent.change(itemInput, { target: { value: text } })
expect(itemInput.value).toEqual(text)
const itemButton = utils.getByText('新增')
fireEvent.click(itemButton)
expect(AddItem).toHaveBeenCalledWith(text)
expect(itemInput.value).toBe("")
})
從外部送 Todos 資料進來,列表就會顯示項目
export function ListBoard(props) {
const { Todos, DeleteItem } = props
return (
<List id="Todo-List-1">
{
Todos.map((item, i) => {
return (
<TodoItem item={item} index={i} key={i} onDelete={() => DeleteItem(i)} />
)
})
}
</List>
)
}
function TodoItem(props) {
const { item, index, onDelete } = props
return (
<ListItem
id={`Item${index}`}
divider
secondaryAction={
<IconButton edge="end" aria-label="delete" title="刪除" onClick={onDelete}>
<FontAwesomeIcon icon="trash" />
</IconButton>
} >
<ListItemText primary={item} />
</ListItem>
)
}
使用 Text 和"刪除"按鈕檢測資料對應生成的項目數量
test('測試顯示 Item 數量', () => {
const counts = 10
const Todos = Array.from(Array(counts), (v, i) => `Item-Text-${i}`)
const utils = render(<ListBoard Todos={Todos} DeleteItem={() => { }} />)
expect(utils.getAllByText(/Item-Text-/i)).toHaveLength(counts)
expect(utils.getAllByTitle('刪除')).toHaveLength(counts)
})
點擊項目後方的山按鈕會觸發刪除 function,且會將刪除的 index 傳送過去
test('刪除 Item 按鈕', () => {
const counts = 10, deleted = 2
const Todos = Array.from(Array(counts), (v, i) => `Item-Text-${i}`)
const DeleteItem = jest.fn()
const utils = render(<ListBoard DeleteItem={DeleteItem} Todos={Todos} />)
const deleteButton = utils.getAllByTitle('刪除')
expect(deleteButton).toHaveLength(counts)
fireEvent.click(deleteButton[deleted])
expect(DeleteItem).toHaveBeenCalledTimes(1)
expect(DeleteItem).toHaveBeenCalledWith(deleted)
})
test('整合測試新增及刪除', () => {
const utils = render(<TodoList get_todolist={() => { }} />)
const itemInput = utils.getByLabelText('Item-Name')
const text = "Item-Text-1"
fireEvent.change(itemInput, { target: { value: text } })
const itemButton = utils.getByText('新增')
fireEvent.click(itemButton)
expect(screen.getByText(text)).toBeInTheDocument()
const item = utils.getByText(text).closest('li')
const deleteButton = item.querySelector('button[title="刪除"]')
fireEvent.click(deleteButton)
expect(item).not.toBeInTheDocument()
})
點擊"送出"按鈕會觸發送出 function 將目前資料傳出去
export function Content(props) {
const { Todos, AddItem, DeleteItem, onSubmit } = props
return (
<>
<CardContent>
<AddListItem AddItem={AddItem} />
</CardContent>
<CardContent>
<ListBoard Todos={Todos} DeleteItem={DeleteItem} />
</CardContent>
<CardActions className="jcc">
<Button onClick={() => onSubmit(Todos)} variant="contained">送出</Button>
</CardActions>
</>
)
}
test('送出 Todo List 按鈕', () => {
const counts = 10
const Todos = Array.from(Array(counts), (v, i) => `Item-Text-${i}`),
onSubmit = jest.fn()
const utils = render(<Content Todos={Todos} AddItem={() => { }} DeleteItem={() => { }} onSubmit={onSubmit} />)
const submitButton = utils.getByText('送出')
fireEvent.click(submitButton)
expect(onSubmit).toHaveBeenCalledWith(Todos)
})
測試寫完了~執行看看結果~
npm run test
後端比較簡易一點,以 express 使用 json 儲存 TodoList 傳回前端
完整專案在 github Day 24 - backend
index.js
const express = require('express')
const app = express()
const cors = require('cors')
const http = require('http')
port = 3000
app.use(cors())
app.use(express.json())
const todoList = require('./todoList.js')
app.use('/api', todoList)
app.get('/', function (req, res) {
res.send('Server is Running!')
})
http.createServer(app).listen(port, function () {
console.log(`Example app listening on port ${port}!`)
})
todoList.js
get 會取用 json 檔出來回傳給前端,put 會將 request 資料存進 json
const router = require('express').Router()
const fs = require('fs')
const route = '/todolist'
router.get(route, (req, res, next) => {
let data = JSON.parse(fs.readFileSync('./data/todos.json', 'utf8'))
res.json({ data }).end()
})
router.put(route, (req, res, next) => {
try {
fs.writeFileSync('./data/todos.json', JSON.stringify(req.body.data), 'utf8')
res.status(200).end()
}
catch (error) {
res.json({ msg: error }).end()
throw new Error("error", error)
}
})
module.exports = router
最後就推上 GitLab~
Groups: test-web
Project: frontend
Project: backend
今天一堆 code 好雜...
下一篇再處理 image 吧~
還有... 測試真的是一個大坑,想要惡補寫進來但補不完阿...