TDD:
Test-Driven Development(测试驱动开发)
编写某个功能的代码之前先编写测试代码,仅编写使测试通过的功能代码,通过测试来推动整个开发的进行
BDD:
Behavior-Driven Development(行为驱动开发)使用自然语言来描述系统功能和业务逻辑,根据描述步骤进行功能开发,然后编写的测试代码

test('精准匹配', () => {
expect(2 + 2).toBe(4)
})
test('对象匹配', () => {
const data = { one: 1 }
data['two'] = 2
expect(data).toEqual({ one: 1, two: 2 })
})
test('相反匹配', () => {
const a = 10
const b = 20
expect(a + b).not.toBe(50)
})
test('布尔匹配', () => {
const B = null
expect(B).toBeFalsy()
expect(B).toBeNull()
expect(B).not.toBeUndefined()
expect(B).not.toBeTruthy()
})
test('等价匹配', () => {
const A = 2,
B = 2
expect(A + B).toBeGreaterThan(3)
expect(A + B).toBeLessThan(5)
const F = 0.1 + 0.2
expect(F).toBeCloseTo(0.3)
})
test('字符串匹配', () => {
const str = 'abcd'
expect(str).toMatch(/ab/)
})
test('数组匹配', () => {
const List = ['hello', 'world', 'one', 'two', 'three', 'four']
expect(List).toContain('one')
})
function Err() {
throw new Error('抛出错误')
}
test('错误匹配', () => {
expect(() => Err()).toThrow(/错误/)
})

import { fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
上面 fireEvent 与 userEvent 有很多相似的地方, 实际上,userEvent是对 fireEvent补充, userEvent 从用户角度模拟交互行为
import {screen, render} from '@testing-library/react'
screen.debug()
const { debug } = render(<Demo click={fn} />)
debug()
点击模拟 - 被测文件
import React, { useState } from 'react'
export type ButtonProps = {
onClick?: () => void
}
const Button = (props: ButtonProps) => {
const [btnState, setBtnState] = useState<boolean>(false)
const inSideClick = () => setBtnState((state) => !state)
return (
<div>
<button onClick={props.onClick('112')}>Click</button>
<button onClick={inSideClick} data-testid="toggle">
{btnState ? '点击了' : '未点击'}
</button>
</div>
)
}
export default Button
点击模拟 - 测试用例
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import Button, { ButtonProps } from './index'
const BTNProps: ButtonProps = {
onClick: jest.fn(),
}
describe('onClick 测试', () => {
test('传入点击', () => {
// 渲染 React DOM
render(<Button {...BTNProps}></Button>)
// 在 screen 找到需要断言的元素
const element = screen.getByText('Click') as HTMLButtonElement
// 断言 结果与预期一致性
expect(element.tagName).toEqual('BUTTON')
// 模拟点击
fireEvent.click(element)
// 断言 是否已经被点击
expect(BTNProps.onClick).toHaveBeenCalled()
// 被点击次数
expect(BTNProps.onClick).toBeCalledTimes(1)
// 点击传参测试
expect(BTNProps.onClick).toBeCalledWith('112') // 参数
})
// 内部点击事件是否触发,可以通过DOM变化,间接测试
test('内部点击', () => {
// 渲染 React dom
render(<Button></Button>)
// 通过 test id 获取渲染树元素
const element = screen.getByTestId('toggle') as HTMLButtonElement
// 断言 '未点击' 文本内容是否存在
expect(element).toHaveTextContent('未点击')
// 模拟点击
fireEvent.click(element)
// 断言 是否点击
expect(element).toHaveTextContent('点击了')
})
})
被测文件: 这里使用上面模拟点击的 被测文件
// 测试用例
import React from 'react'
import { render, screen } from '@testing-library/react'
import Button from '../onClick/index'
test('快照测试', () => {
render(<Button></Button>)
const element = screen.getByTestId('toggle') as HTMLButtonElement
expect(element).toMatchSnapshot()
})
测试结果: 生成一个__snapshots__文件夹

import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
// 被测试组件
function Demo(props: any) {
return (
<>
<button onClick={() => props?.click('112')}>click</button>
<input type="text" data-testid="input" />
<input type="text" data-testid="blur" />
</>
)
}
// input 测试用例
describe('input 测试', () => {
test('input', async () => {
// 渲染组件
render(<Demo />)
// 获取节点
const input = screen.getByTestId('input') as HTMLInputElement
// 模拟输入
fireEvent.change(input, { target: { value: '1223' } })
// 判断值
expect(input.value).toBe('1223')
})
test('blur', async () => {
// 渲染组件
render(<Demo />)
// 获取节点
const input = screen.getByTestId('blur') as HTMLInputElement
// 模拟输入激活焦点
input.blur()
})
test('userEvent input', () => {
const fn = jest.fn()
const { container, debug } = render(<Demo click={fn} />)
// 断点使用
debug()
// 查找 DOM
const btn = container.querySelector('button') as HTMLButtonElement
// 模拟事件点击
userEvent.click(btn)
// 调用 次数
expect(fn).toBeCalledTimes(1)
})
})
import React, { useCallback, useState } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react-dom/test-utils'
test('selectOptions', async () => {
render(
<select multiple>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>,
)
await userEvent.selectOptions(screen.getByRole('listbox'), ['1', 'C'])
expect(screen.getByRole('option', {name: 'A'}).selected).toBe(true)
expect(screen.getByRole('option', {name: 'B'}).selected).toBe(false)
expect(screen.getByRole('option', {name: 'C'}).selected).toBe(true)
})
import React, { useCallback, useState } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react-dom/test-utils'
test('deselectOptions', async () => {
render(
<select multiple>
<option value="1">A</option>
<option value="2" selected>
B
</option>
<option value="3">C</option>
</select>,
)
await userEvent.deselectOptions(screen.getByRole('listbox'), '2')
expect(screen.getByText('B').selected).toBe(false)
})
import React, { useCallback, useState } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react-dom/test-utils'
// 被测试组件
function Demo() {
const [flag, setFlag] = useState(false)
const clickHandle = useCallback(() => {
setFlag(true)
setTimeout(() => {
setFlag(false)
}, 2000)
}, [setFlag])
return (
<>
<button className={`${flag ? 'disabled' : ''}`} onClick={clickHandle}>
click
</button>
</>
)
}
// 模拟定时器测试
describe('mock time', () => {
test('setTimeout', async () => {
// 模拟定时器
jest.useFakeTimers()
// 渲染组件
render(<Demo />)
// 查找元素
const btn = screen.getByRole('button')
// 断言
expect(btn).not.toHaveClass('disabled')
// 模拟点击
userEvent.click(btn)
// 断言 dom 中是否包含 class
expect(btn).toHaveClass('disabled')
// act 是 test-utils 的一个异步方法
act(() => {
// 模拟 准确时间
// jest.advanceTimersByTime(2000)
// 模拟所有的定时器
jest.runAllTimers()
})
expect(btn).not.toHaveClass('disabled')
// debug 方法
})
})
import { render } from '@testing-library/react'
import React from 'react'
function Demo(props) {
return <div>{props.num}</div>
}
test('re render', () => {
const { container, debug, rerender } = render(<Demo num={2} />)
expect(container.querySelector('div').textContent).toBe('2')
// 通过 rerender 字段实现重新渲染
rerender(<Demo num={5} />)
expect(container.querySelector('div').textContent).toBe('5')
})
通过 @testing-library/react-hooks 这个库实现自定义hooks测试
import { renderHook, act } from '@testing-library/react-hooks'
import React, { useEffect, useState } from 'react'
function useSum(init) {
const [count, setCount] = useState(init)
const [resNum, setResNum] = useState()
useEffect(() => {
setResNum(count + 10)
}, [count])
return { resNum, setCount }
}
test('hooks', () => {
const { result } = renderHook(() => useSum(0))
expect(result.current.resNum).toBe(10)
act(() => {
result.current.setCount(100)
})
expect(result.current.resNum).toBe(110)
})
被测试组件
import React from 'react'
enum Types {
red = 'red',
green = 'green',
blue = 'rgb(34, 35, 35)',
}
function Demo(props) {
return (
<>
<button style={{ background: props.types }}>btn</button>
</>
)
}
正常测试
import { render, screen, waitFor } from '@testing-library/react'
import { act } from 'react-dom/test-utils'
enum Types {
red = 'red',
green = 'green',
blue = 'rgb(34, 35, 35)',
}
test('btn background', async () => {
const { rerender } = render(<Demo types={'red'} />)
expect(screen.getByRole('button').style.background).toBe(Types.red)
act(() => {
rerender(<Demo types={'green'} />)
})
expect(screen.getByRole('button').style.background).toBe(Types.green)
rerender(<Demo types={'#222323'} />)
expect(screen.getByRole('button').style.background).toBe(Types.blue)
})
使用 test.each 简化测试
enum Types {
red = 'red',
green = 'green',
blue = 'rgb(34, 35, 35)',
}
test.each([
['red', Types.red],
['green', Types.green],
['#222323', Types.blue],
])('test each', (type, expected) => {
render(<Demo types={type} />)
expect(screen.getByRole('button').style.background).toBe(expected)
})
测试 redux - 被测文件
import React from 'react'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
export type StoreType = {
userInfo: {
age: number
name: string
id: string
}
}
const UserInfoPart = () => {
const userInfo = useSelector((store: StoreType) => store.userInfo)
const jump = useHistory()
const jumpHandle = () => jump.push('a/b/c')
return (
<div>
<h3 onClick={jumpHandle}>ID: {userInfo.id}</h3>
<p>姓名: {userInfo.name}</p>
<p>年龄:{userInfo.age}</p>
</div>
)
}
export default UserInfoPart
测试 redux - 测试文件
通过 redux-mock-store 库 实现 redux 模拟测试
import React from 'react'
import { render, screen } from '@testing-library/react'
import UserInfoPart, { StoreType } from './index'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
// 定义初始化数据
const initState: StoreType = {
userInfo: {
age: 18,
name: 'xiaoming',
id: 'xm-110-2',
},
}
// store 数据模拟
const mockStore = configureStore([])
const store = mockStore(initState)
describe('模拟redux', () => {
test('验证姓名,年龄', () => {
render(
<Provider store={store}>
<UserInfoPart />
</Provider>
)
const element = screen.getByText('姓名: xiaoming')
const el = screen.getByText(`ID: xm-110-2`)
expect(el.tagName).toBe('H3')
expect(element.tagName).toEqual('P')
})
})
被测组件
import React, { useCallback, useEffect, useState } from 'react'
function Demo() {
const [data, setData] = useState(['11'])
const [err, setErr] = useState('')
const clickHandle = useCallback(async () => {
fetch('/user/submit', {
method: 'POST',
body: JSON.stringify({
useranme: '123',
}),
}).then((res) => {
if (res.status === 400) {
console.log(res.status)
setErr('提交错误')
}
})
}, [setErr])
const fetchData = () => {
fetch('/list', {
method: 'POST',
})
.then((res: any) => {
return res.json()
})
.then((res: any) => {
setData(res)
})
}
useEffect(() => {
fetchData()
}, [fetchData])
return (
<>
<span data-testid="err">{err}</span>
<button onClick={clickHandle}>click</button>
<div>
<ul data-testid="list">
{data.length &&
data.map((i: string, index: number) => {
return <li key={index}>{i}</li>
})}
</ul>
</div>
</>
)
}
export default Demo
测试用例
模拟请求 需要通过 msw 库实现
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { setupServer } from 'msw/node'
import { rest } from 'msw'
import Demo from './list'
import { act } from 'react-dom/test-utils'
import { runServer } from './mocks'
runServer(beforeAll, afterAll, afterEach)
// const server = setupServer()
// const server = setupServer(
// rest.post('/list', (req, res, ctx) => {
// return res(ctx.json(['1', '2', '3']))
// })
// )
// beforeAll(() => server.listen())
// afterAll(() => server.close())
// afterEach(() => server.resetHandlers())
test('data', async () => {
render(<Demo />)
await waitFor(() => {
const list = screen.getByTestId('list')
expect(list.children).toHaveLength(3)
})
})
// test('submit', async () => {
// render(<Demo />)
// // await waitFor(() => {
// // const list = screen.getByTestId('list')
// // expect(list.children).toHaveLength(3)
// // })
// await waitFor(() => {
// const btn = screen.getByRole('button')
// userEvent.click(btn)
// })
// await waitFor(() => {
// const err = screen.getByTestId('err')
// expect(err.textContent).toBe('提交错误')
// })
// })
父组件 : Index
import React from "react";
// 引入子组件
import Child from "child";
const Index = () => {
function callBack(message: string = "") {
console.log(`来自子组件的消息是:${message}`);
}
return (
<div className="jest-demo">
<Child callBack={callBack} />
</div>
);
};
export default Index;
子组件 child
import React, { useEffect } from "react";
type iPropsType = {
callBack: Function;
};
const Child= (props: iPropsType) => {
useEffect(() => {
props.callBack("我是正经的子组件");
}, []);
return <div>子组件</div>;
};
export default Child;
测试用例
import React from "react";
import { render } from "@testing-library/react";
import JestDemo from "../index";
// 注意这里 child 组件是需要被模拟的, 使用 mock_component 组件代替 child 组件
jest.mock("./child", () => require("./mock_component").default);
describe("组件mock单测", () => {
test("mock组件", async () => {
const { container } = render(<JestDemo />);
expect() // 断言逻辑
});
});
mock的组件 : mock_component
import React, { useEffect } from "react";
type iPropsType = {
callBack: Function;
};
const Index = (props: iPropsType) => {
useEffect(() => {
props.callBack("我是MOCK的子组件");
}, []);
return <div>页面</div>;
};
export default Index;
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。
我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere
Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test
我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r
我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel
只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您
我有:When/^(?:|I)follow"([^"]*)"(?:within"([^"]*)")?$/do|link,selector|with_scope(selector)doclick_link(link)endend我打电话的地方:Background:GivenIamanexistingadminuserWhenIfollow"CLIENTS"我的HTML是这样的:CLIENTS我一直收到这个错误:.F-.F--U-----U(::)failedsteps(::)nolinkwithtitle,idortext'CLIENTS'found(Capybara::Element