作为工程师,我们希望构建有用的东西,但随着我们创建的每一个新功能,我们不可避免地会增加应用程序的大小和复杂性。
随着产品的发展,手动(例如用手)测试受更改影响的每个功能变得越来越耗时。
缺乏自动化测试导致我们要么花费太多时间并降低运输速度,要么花费太少来节省速度,从而导致积压中出现新的错误以及来自 PagerDuty 的深夜电话。
相反,计算机可以通过编程重复执行相同的操作。那么,让我们将测试委托给计算机吧!
测试金字塔思想提出了三种主要类型的测试:单元测试、集成测试和端到端测试。让我们深入研究每种类型并了解为什么我们需要每种类型。
单元是您单独测试的一小段逻辑(不依赖其他组件)。
单元测试很快。他们在几秒钟内完成。隔离允许他们在任何时间点、本地和 CI 上运行它们,而无需启动依赖的服务/进行 API 和数据库调用。
单元测试示例:接受两个数字并将它们相加的函数。我们想用不同的参数调用它并断言返回的值是正确的。
// Function "sum" is the unit const sum = (x, y) => x + y test('sums numbers', () => { // Call the function, record the result const result = sum(1, 2); // Assert the result expect(result).toBe(3) }) test('sums numbers', () => { // Call the function, record the result const result = sum(5, 10); // Assert the result expect(result).toBe(15) })
一个更有趣的例子是 React 组件,它在 API 请求完成后呈现一些文本。我们需要模拟 API 模块以返回测试所需的值、渲染组件并断言渲染的 HTML 具有我们需要的内容。
// "MyComponent" is the unit const MyComponent = () => { const { isLoading } = apiModule.useSomeApiCall(); return isLoading ? <div>Loading...</div> : <div>Hello world</div> } test('renders loading spinner when loading', () => { // Mocking the API module, so that it returns the value we need jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Loading...').toBeInTheDocument() }) test('renders text content when not loading', () => { // Mocking the API module jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Hello world').toBeInTheDocument() })
当您的单元与其他单元(依赖项)交互时,我们将其称为集成。这些测试比单元测试慢,但它们测试应用程序各部分的连接方式。
集成测试示例:在数据库中创建用户的服务。这要求执行测试时数据库实例(依赖项)可用。我们将测试该服务是否可以从数据库创建和检索用户。
import db from 'db' // We will be testing "createUser" and "getUser" const createUser = name => db.createUser(name) // creates a user const getUser = name => db.getUserOrNull(name) // retrieves a user or null test("creates and retrieves users", () => { // Try to get a user that doesn't exist, assert Null is returned const nonExistingUser = getUser("i don't exist") expect(nonExistingUser).toBe(null); // Create a user const userName = "test-user" createUser(userName); // Get the user that was just created, assert it's not Null const user = getUser(userName); expect(user).to.not.be(null) })
当我们测试完全部署的应用程序时,这是一个端到端测试,其中所有依赖项都可用。这些测试最好地模拟实际用户行为,并允许您捕获应用程序中所有可能的问题,但它们是最慢的测试类型。
每当您想要运行端到端测试时,您都必须配置所有基础设施并确保第三方提供商在您的环境中可用。
您只想将它们用于应用程序的关键任务功能。
让我们看一个端到端的测试示例:登录流程。我们想要转到应用程序,填写登录详细信息,提交,然后查看欢迎消息。
test('user can log in', () => { // Visit the login page page.goto('https://example.com/login'); // Fill in the login form page.fill('#username', 'john'); page.fill('#password', 'some-password'); // Click the login button page.click('#login-button'); // Assert the welcome message is visible page.assertTextVisible('Welcome, John!') })
请记住,端到端测试比集成慢,集成测试比单元测试慢。
如果您正在开发的功能是关键任务,请考虑至少编写一个端到端测试(例如在开发身份验证流程时检查登录功能如何工作)。
除了关键任务流程之外,我们还希望测试尽可能多的边缘情况和功能的各种状态。集成测试使我们能够测试应用程序的各个部分如何协同工作。
对端点和客户端组件进行集成测试是一个好主意。端点应该执行操作,产生预期的结果,并且不会抛出任何意外的错误。
客户端组件应该显示正确的内容并按照您期望的方式响应用户交互。
最后,我们什么时候应该选择单元测试?所有可以单独测试的小函数,例如对数字求和的sum
、呈现<button>
标记的Button
,都是单元测试的绝佳候选者。如果您遵循测试驱动开发方法,那么单元就是完美的。
写一些测试! (但从小处开始)
执行上述操作一次即可了解其工作原理。然后,在某些功能/错误工作期间再次执行此操作。然后分享给你的同事,让大家都写测试,节省时间,晚上睡得更好!