有时开发者不会实现测试驱动开发 (TDD) 方法,仅仅因为他们正在构建的项目很小,不需要测试框架。但是,这是一个致命的错误。大型项目都是从小项目开始的。一旦小项目的价值得到认可,小项目就会扩展成中型或大型项目。
TDD 是一种强制性的开发方法,它确保您的项目在每个阶段都满足所有需求和安全标准。如果您没有实现 TDD,因为这需要更多纪律,那么这篇文章就适合您。本文将教您如何使用 CI/CD 工具自动执行 TDD 测试。像 Travis CI 这样的 CI/CD 工具可以简化 TDD 的实现并缩短实施时间。
测试驱动开发是一种软件开发理念,它强调在编写应用程序代码之前编写测试,以确保代码库始终得到测试和验证。TDD 包含以下步骤,我们将在本文的下一部分中详细介绍:
TDD 是迭代的,因为它遵循一个循环,不断测试代码并重构代码,直到代码满足需求和客户的期望。TDD 是以客户反馈为驱动的,客户是第一位的。
在测试之前编写代码的问题是,很容易迷失在开发过程中,忘记客户的期望。当你是一个完美主义者时,情况会更糟。你不断添加和删除代码块,仅仅因为你认为代码不足以满足功能。当你开始测试代码时,测试很大,并输出无休止的复杂错误。
假设您正在开发一个电子商务网站。您从定义组件和设置网站逻辑开始。完成网站开发后,您开始编写测试。在编写测试时,您意识到忘记添加对用户输入的验证检查,最糟糕的事情发生了:当提交无效数据时,代码没有给出预期的输出,导致订单处理错误和潜在的数据损坏。
如果您遵循 TDD,这种情况会好转,因为它不仅为您提供了一个清晰定义的工作流程,而且还引导您完成每一步,以防止您错过关键目标和应用程序期望。
TDD 在消除技术债务方面非常有效,因为在错误还很小且不那么复杂时就能捕获它们。代码在每一步都被优化,以避免日后需要昂贵维护的复杂代码。这也能节省您的时间,因为在应用程序架构中消除广泛存在的大型缺陷非常耗时。
TDD 是一种源于敏捷开发和极限编程 (XP) 的方法。TDD 非常适合敏捷开发工作流程,因为:
在我们开始自动化 TDD 过程之前,让我们使用 GitHub 设置 Travis CI。Travis CI 提供 30 天的免费试用,您可以使用它来探索 CI/CD 如何提升您的测试工作流程。访问 Travis CI 并 使用您的 GitHub 帐户登录。GitHub 会要求您授权并允许 Travis CI 访问您的仓库,请确保您授权 Travis CI。
使用 GitHub 登录 Travis CI 后,您就可以开始了。接下来,导航到 Travis 仪表盘,熟悉 Travis CI。
接下来,您需要编写 .travis.yml 或 Travis 文件来设置持续集成配置。此文件存储在仓库根路径中。Travis 文件定义了 Travis CI 如何构建应用程序环境。没有此文件,您的 Travis CI 管道就不存在。此文件包含以下重要的管道配置组件:
language: node_js
node_js:
- 14
install:
- npm install
script:
- npm test
使用 .travis.yml 文件,Travis CI 将克隆您的仓库并创建一个适合您的项目依赖项的虚拟环境。Travis CI 使用您仓库中的测试来测试您的代码。确保您的测试存储在正确的文件夹中,例如 Javascript 测试位于 tests/ 文件夹中或以 .tests.js 扩展名结尾。这将根据您使用的测试框架或库而有所不同。
在本节中,您将学习如何将 TDD 与 Travis CI/CD 集成。将 TDD 与持续集成 (CI) 和持续交付 (CD) 实践相结合,可以确保在现代开发工作流程中充分发挥 TDD 的优势。
为了使本教程更加实用,我们将测试一个简单的电子商务购物车组件。您不必构建应用程序就能执行以下部分中列出的说明。只要您拥有带有测试的代码,您就可以开始了。如果您没有代码,请继续从本教程中复制代码和测试以创建您自己的文件。
您的 GitHub 仓库结构需要包含以下文件:
此外,确保您已设置测试框架并下载了其必要的依赖项。以下是购物车组件的 package.json 文件,该项目使用 Jest 测试框架来执行和编译测试。
{
"name": "TDD",
"repository": {
"type": "git",
"url": "https://github.com/xTrilton/Integrating-CI-CD-with-TDD"
},
"description": "Simple demo showing how to integrate CI with TDD",
"author": "boemo",
"version": "1.0.0",
"devDependencies": {
"react": "^18.2.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"lucide-react": "^0.400.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"@testing-library/jest-dom": "^5.16.5"
},
"scripts": {
"test": "react-scripts test"
}
}
TDD 循环也称为红绿重构。红色代表测试阶段,绿色代表实现代码阶段。以下是测试驱动开发方法的三个核心阶段。
这是循环中最具挑战性的阶段。您需要为不存在的代码创建单元测试。测试将失败,因为代码还没有编写。这个阶段被称为红色阶段,因为当您尝试编译测试时,单元测试框架将显示红色的错误文本。在您实现代码之前,一切都将是红色的。这个阶段的本质不是获得编译测试,而是为代码的功能和行为设置护栏。
我们将要测试的购物车简单地显示产品,并为用户提供按钮,让他们可以使用 React 将产品添加到购物车或从购物车中移除产品。一旦您将以下代码添加到与 Travis CI 连接的仓库中,并在 Github 上进行提交,Travis CI 将在云上触发一个测试管道。
以下是测试购物车组件的 app.tests.js。
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShoppingCartComponent from './ShoppingCartComponent';
describe('ShoppingCartComponent', () => {
test('renders the component', () => {
render(<ShoppingCartComponent />);
expect(screen.getByText('Shopping Cart')).toBeInTheDocument();
expect(screen.getByText('Products')).toBeInTheDocument();
expect(screen.getByText('Cart')).toBeInTheDocument();
});
test('displays all products', () => {
render(<ShoppingCartComponent />);
expect(screen.getByText('Product 1 - $10')).toBeInTheDocument();
expect(screen.getByText('Product 2 - $15')).toBeInTheDocument();
expect(screen.getByText('Product 3 - $20')).toBeInTheDocument();
});
test('adds a product to the cart', () => {
render(<ShoppingCartComponent />);
const addButtons = screen.getAllByRole('button', { name: '' });
fireEvent.click(addButtons[0]); // Add Product 1
expect(screen.getByText('Product 1 - $10 x 1')).toBeInTheDocument();
expect(screen.getByText('Total: $10')).toBeInTheDocument();
});
test('increases quantity when adding the same product', () => {
render(<ShoppingCartComponent />);
const addButtons = screen.getAllByRole('button', { name: '' });
fireEvent.click(addButtons[0]); // Add Product 1
fireEvent.click(addButtons[0]); // Add Product 1 again
expect(screen.getByText('Product 1 - $10 x 2')).toBeInTheDocument();
expect(screen.getByText('Total: $20')).toBeInTheDocument();
});
test('removes a product from the cart', () => {
render(<ShoppingCartComponent />);
const addButtons = screen.getAllByRole('button', { name: '' });
fireEvent.click(addButtons[0]); // Add Product 1
fireEvent.click(addButtons[0]); // Add Product 1 again
const removeButtons = screen.getAllByRole('button', { name: '' }).slice(3); // Get remove buttons
fireEvent.click(removeButtons[0]); // Remove Product 1 once
expect(screen.getByText('Product 1 - $10 x 1')).toBeInTheDocument();
expect(screen.getByText('Total: $10')).toBeInTheDocument();
});
test('removes product completely when quantity reaches 0', () => {
render(<ShoppingCartComponent />);
const addButtons = screen.getAllByRole('button', { name: '' });
fireEvent.click(addButtons[0]); // Add Product 1
const removeButtons = screen.getAllByRole('button', { name: '' }).slice(3); // Get remove buttons
fireEvent.click(removeButtons[0]); // Remove Product 1
expect(screen.queryByText('Product 1 - $10 x 1')).not.toBeInTheDocument();
expect(screen.getByText('Total: $0')).toBeInTheDocument();
});
test('calculates total price correctly with multiple products', () => {
render(<ShoppingCartComponent />);
const addButtons = screen.getAllByRole('button', { name: '' });
fireEvent.click(addButtons[0]); // Add Product 1
fireEvent.click(addButtons[1]); // Add Product 2
fireEvent.click(addButtons[1]); // Add Product 2 again
fireEvent.click(addButtons[2]); // Add Product 3
expect(screen.getByText('Total: $60')).toBeInTheDocument();
});
});
上面的测试函数会导致错误,因为要使测试成功,需要在应用程序代码文件中定义许多组件。以下是一张显示 Travis CI 仪表盘中的作业日志的图片,该日志显示了购物车测试如何失败。每当 Travis CI 完成构建您的项目时,它都会向您发送一封电子邮件,通知您测试是否失败或通过。
在这个阶段,您的目标是编写足够的代码,使它通过上一阶段设置的测试。不多不少。这个阶段保持一切简单,因为重构阶段是清理和优化代码的阶段。开发人员必须编译代码,然后启动单元测试框架来测试代码。代码必须通过测试才能进入下一阶段。以下是 app.js 文件中购物车组件的代码,
import React, { useState } from 'react';
import { Plus, Minus, ShoppingCart } from 'lucide-react';
const ShoppingCartComponent = () => {
const [cart, setCart] = useState([]);
const [products] = useState([
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 15 },
{ id: 3, name: 'Product 3', price: 20 },
]);
const addToCart = (product) => {
setCart((prevCart) => {
const existingItem = prevCart.find((item) => item.id === product.id);
if (existingItem) {
return prevCart.map((item) =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
}
return [...prevCart, { ...product, quantity: 1 }];
});
};
const removeFromCart = (productId) => {
setCart((prevCart) => {
const existingItem = prevCart.find((item) => item.id === productId);
if (existingItem.quantity === 1) {
return prevCart.filter((item) => item.id !== productId);
}
return prevCart.map((item) =>
item.id === productId ? { ...item, quantity: item.quantity - 1 } : item
);
});
};
const getTotalPrice = () => {
return cart.reduce((total, item) => total + item.price * item.quantity, 0);
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Shopping Cart</h1>
<div className="grid grid-cols-2 gap-4">
<div>
<h2 className="text-xl font-semibold mb-2">Products</h2>
{products.map((product) => (
<div key={product.id} className="flex justify-between items-center mb-2">
<span>{product.name} - ${product.price}</span>
<button
onClick={() => addToCart(product)}
className="bg-blue-500 text-white p-2 rounded"
>
<Plus size={16} />
</button>
</div>
))}
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Cart</h2>
{cart.map((item) => (
<div key={item.id} className="flex justify-between items-center mb-2">
<span>
{item.name} - ${item.price} x {item.quantity}
</span>
<button
onClick={() => removeFromCart(item.id)}
className="bg-red-500 text-white p-2 rounded"
>
<Minus size={16} />
</button>
</div>
))}
<div className="mt-4">
<strong>Total: ${getTotalPrice()}</strong>
</div>
</div>
</div>
</div>
);
};
export default ShoppingCartComponent;
编写并添加代码后,即使仍然存在问题,测试也会通过…
问题是购物车组件代码正在使用过时的 React 库,如下图所示。一旦我们将 ReactDOMTestUtilis.act 替换为来自 react 的 act 函数,由过时库导致的所有问题都将得到解决。
由于上一个阶段没有遵循任何代码优化和安全标准,重构阶段专注于完善代码:删除代码重复项,提高代码可读性、可维护性和整体代码安全性。最终,代码必须具有良好的标准并免受缺陷的侵害。
在此阶段,当您破坏功能时,您将收到来自 Travis CI 的警报,因为管道会自动运行以验证您的新代码添加。因此,您不必担心在删除重复项和不安全的代码时破坏满足需求的代码。
对于重构,您可以使用多个库和工具来改进代码。像 JSHint 这样的代码 linting 工具擅长识别语法错误和重复项。
将 Travis CI 与测试驱动开发相结合,是一个改变游戏规则的组合。您所做的每次提交都会对代码进行测试,测试结果会显示在仪表盘上。在本教程中,我们不需要编写任何吓人的命令就能实现 TDD 自动化。这对初学者和自学成才的 DevOps 工程师来说是一个好消息。
这就是自动化的真正荣耀所在。它使开发人员能够尽早发现错误,并在错误导致造成公司数百万美元损失的数据泄露之前识别出错误。