iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0
DevOps

前端轉生~到了實驗室就要養幾隻可愛鯨魚:自架 Kubernetes 迷航日記系列 第 24

Day 24 — 做點小小研究:測試開發網站流程

  • 分享至 

  • xImage
  •  

可愛鯨魚

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>
    )
}

接下來依照功能跟著列出測試方法~


1. 新增 Todo Item

輸入項目名稱 (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("")
})

2. 取得資料顯示 Todo List

從外部送 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)
})

3. 刪除 Todo Item

點擊項目後方的山按鈕會觸發刪除 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)
})

4. 整合新增、顯示、刪除

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()
})

5. 送出後傳送到後端儲存

點擊"送出"按鈕會觸發送出 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


Ref


今天一堆 code 好雜...
下一篇再處理 image 吧~

還有... 測試真的是一個大坑,想要惡補寫進來但補不完阿... /images/emoticon/emoticon06.gif


上一篇
Day 23 — 實驗室前置作業:GitLab 開發前置設定 (SSH 通道)
下一篇
Day 25 — 開發小小貨櫃:撰寫 Dockerfile
系列文
前端轉生~到了實驗室就要養幾隻可愛鯨魚:自架 Kubernetes 迷航日記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言