tdd 使用详解
One thing I struggled with when I started learning React was testing my web apps in a way that is both useful and intuitive. I used Enzyme with Jest to shallow render a component every time I wanted to test it.
我开始学习React时遇到的一件事就是以一种有用且直观的方式测试我的Web应用程序。 每当我想测试组件时,我都将Enzyme与Jest一起使用来浅化组件。
Of course, I was absolutely abusing the snapshot testing feature.
当然,我绝对在滥用快照测试功能。
Well, at least I wrote a test right?
好吧,至少我写了一个测试对吗?
You might have heard somewhere that writing unit and integration tests will improve the quality of the software you write. Having bad tests, on the other hand, breeds false confidence.
您可能在某处听说过编写单元和集成测试可以提高所编写软件的质量。 另一方面,进行不良测试会滋生错误的信心。
Recently, I attended a workshop through workshop.me with Kent C. Dodds where he taught us how to write better integration tests for React applications.
最近,我与Kent C. Dodds一起参加了一个来自workshop.me的研讨会 ,在那里他教我们如何为React应用程序编写更好的集成测试。
He also tricked us into using his new testing library, in favor of its emphasis on testing the application in the same way that a user would encounter it.
他还欺骗我们使用他的新测试库 ,以强调它以用户会遇到的相同方式测试应用程序。
In this article, we will learn to exercise TDD in order to build solid React applications by creating a comment feed. Of course, this process applies to just about all software development, not just React or JavaScript apps.
在本文中,我们将学习练习TDD,以通过创建评论提要来构建可靠的React应用程序。 当然,此过程几乎适用于所有软件开发,而不仅仅是React或JavaScript应用程序。
入门 (Getting Started)
We’re going to start off by running create-react-app
and installing the dependencies. My assumption is that if you’re reading an article about testing applications, you’re probably already a familiar with installing and starting up JavaScript projects. I’ll be using yarn
rather than npm
here.
我们将从运行create-react-app
并安装依赖项开始。 我的假设是,如果您正在阅读有关测试应用程序的文章,那么您可能已经熟悉安装和启动JavaScript项目。 我将在这里使用yarn
而不是npm
。
create-react-app comment-feed
cd comment-feed
yarn
As it stands, we can remove all of the files in the src
directory except for index.js. Then, right inside the src
folder, create a new folder called components
and another folder called containers
.
就目前而言,我们可以删除src
目录中除index.js之外的所有文件。 然后,在src
文件夹中,创建一个名为components
的新文件夹,另一个名为containers
文件夹。
For testing utilities, I am going to build this app using Kent’s React Testing Library. It is a lightweight test utility that encourages the developer to test their application in the same way that it’ll be used.
对于测试实用程序,我将使用Kent的React Testing Library构建此应用程序。 它是一个轻量级的测试实用程序,它鼓励开发人员以与使用它相同的方式来测试其应用程序。
Like Enzyme, it exports a render function, but this render function always does a full mount of your component. It exports helper methods allowing you to locate elements by label or text or even test IDs. Enzyme does that as well with its mount
API, but the abstraction it creates actually offers more options, many of which allow you to get away with testing implementation details.
像酶一样,它导出渲染功能,但是此渲染功能始终会完全装载您的组件。 它导出帮助程序方法,使您可以按标签或文本甚至测试ID定位元素。 酶通过其mount
API也能做到这一点,但是它创建的抽象实际上提供了更多选项,其中许多选项使您无需测试实现细节。
We don’t want to test implementation details anymore. We want to render a component and see if the right things happen when we click or change something on the UI. That’s it! No more directly checking props or state or class names.
我们不想再测试实现细节。 我们想渲染一个组件,看看当我们单击或更改UI上的某些内容时是否发生了正确的事情。 而已! 不再直接检查道具或州或班级名称。
Let’s install them and get to work.
让我们安装它们并开始工作。
yarn add react-testing-library
使用TDD构建评论供稿 (Building the Comment Feed with TDD)
Let’s do this first component TDD-style. Fire up your test runner.
让我们做第一个TDD风格的组件。 启动您的测试运行器。
yarn test --watch
Inside the containers
folder, we are going to add a file called CommentFeed.js. Alongside it, add a file called CommentFeed.test.js. For the very first test, let’s verify that users can create comments. Too soon? Okay, since we don’t have any code yet, we’ll start with a smaller test. Let’s check that we can render the feed.
在containers
文件夹内,我们将添加一个名为CommentFeed.js的文件。 在它旁边,添加一个名为CommentFeed.test.js的文件。 对于第一个测试,让我们验证用户可以创建评论。 太快了? 好的,因为我们还没有任何代码,所以我们将以较小的测试开始。 让我们检查一下是否可以渲染提要。
关于React测试库的一些说明 (Some notes on react-testing-library)
First, let’s note the render function here. It is similar to the way react-dom
renders a component onto the DOM, but it returns an object which we can destructure to get some neat test helpers. In this case, we get queryByText
, which, given some text we expect to see on the DOM, will return that HTML element.
首先,让我们在这里注意render函数。 它类似于react-dom
将组件呈现到DOM上的方式,但是它返回一个对象,我们可以对其进行分解以获得一些简洁的测试助手。 在这种情况下,我们得到queryByText
,给定我们希望在DOM上看到的某些文本,它将返回该HTML元素。
The React Testing Library docs have a hierarchy that should help you decide which query or get method to use. Generally, the order goes like this:
React Testing库文档具有一个层次结构,可以帮助您确定要使用的查询或获取方法。 通常,顺序如下:
getByLabelText
(form inputs)getByLabelText
(表单输入)getByPlaceholderText
(only if your input doesn’t have a label — less accessible!)getByPlaceholderText
(仅当您的输入没有标签时才可访问)getByText
(buttons and headers)getByText
(按钮和标题)getByAltText
(images)getByAltText
(图像)getByTestId
(use this for things like dynamic text or otherwise odd elements you want to test)getByTestId
(用于动态文本或您要测试的其他奇数元素)
Each of these has an associated queryByFoo
that does the same, except won’t fail your test when it doesn’t find an element. Use these if you’re just testing for the existence of an element.
它们中的每一个都有一个关联的queryByFoo
,它们的功能相同,只是在找不到元素时不会使测试失败。 如果您只是在测试元素的存在 ,请使用它们。
If none of these get you exactly what you’re looking for, the render
method also returns the DOM element mapped to the container
property, so you can use it like container.querySelector(‘body #root’)
.
如果这些都不能满足您的需求, render
方法还会返回映射到container
属性的DOM元素,因此您可以像container.querySelector('body #root')
一样使用它。
第一个实施代码 (The First Implementation Code)
Now, the implementation will look fairly simple. We just need to make sure that “Comment Feed” is in the component.
现在,实现看起来相当简单。 我们只需要确保组件中有“ Comment Feed”即可。
It could be worse — I mean, I was about to write this whole article while styling components. Fortunately, tests don’t care too much for styles, so we can focus on our application logic.
可能会更糟—我的意思是,我将在编写组件样式时写整篇文章。 幸运的是,测试并不太在乎样式,因此我们可以专注于应用程序逻辑。
This next test will verify that we can render comments. But we don’t even have any comments, so let’s add in that component too. After the test though.
下一个测试将验证我们可以呈现评论。 但是我们什至没有任何评论,因此我们也要添加该组件。 经过测试虽然。
I’m also going to create a props object to store the data we may reuse in these tests.
我还将创建一个props对象来存储我们可以在这些测试中重用的数据。
In this case, I am checking that the number of comments is equal to the number of items passed into the CommentFeed. It’s trivial, but the failure of the test gives us the opportunity to create the Comment.js file.
在这种情况下,我正在检查注释数是否等于传递到CommentFeed中的项目数。 这很简单,但是测试失败使我们有机会创建Comment.js文件。
This green lights our test suite so we can proceed without fear. All hail TDD, the savior of our kind. It works when we give it an empty array, of course. But what if we give it some real objects?
绿灯点亮了我们的测试套件,因此我们可以继续进行而无需担心。 大家欢呼TDD,我们的救星。 当然,当我们给它一个空数组时,它将起作用。 但是,如果我们给它一些实物呢?
We must update our implementation to actually render stuff. Simple enough now that know where we’re going, right?
我们必须更新实现以实际渲染东西。 现在很简单,知道我们要去哪里,对吗?
Ah look at that, our test is once again passing. Here’s a neat shot of its beauty.
嗯,我们的测试又一次通过了。 这是它美丽的镜头。
Notice how I never once said we should fire up our program with yarn start
? We’re going to keep it that way for a while. The point is, you must feel the code with your mind.
请注意,我从来没有说过应该使用yarn start
程序吗? 我们将保持这种状态一段时间。 关键是,您必须用心去感受代码。
The styling is just what’s on the outside — it’s what is on the inside that counts.
样式就是外在的东西–内在才是最重要的。
Just in case you want to start the app though, update index.js to the following:
万一您想启动该应用程序,请将index.js更新为以下内容:
添加评论表格 (Add Comment Form)
This is where things start getting more fun. This is where we go from sleepily checking for the existence of DOM nodes to actually doing stuff with that and validating behavior. All that other stuff was a warmup.
这是事情开始变得更加有趣的地方。 这是我们从困苦地检查DOM节点的存在到实际用它做事并验证行为的地方 。 其他所有的事情都是热身。
Let’s start by describing what I want from this form. It should:
让我们从描述此表单的内容开始。 这应该:
- contain a text input for the author 包含作者的文本输入
- contain a text input for then comment itself 包含一个文本输入,然后进行注释
- have a submit button 有一个提交按钮
- eventually call the API or whatever service handles creating and storing the comment. 最终调用API或任何处理创建和存储注释的服务。
We can take down this list in a single integration test. For the previous test cases we took it rather slowly, but now we’re going to pick up the pace and try to nail it in one fell swoop.
我们可以在单个集成测试中列出该列表。 对于以前的测试用例,我们采取了相当缓慢的措施,但是现在我们要加快步伐,并尝试将其固定下来。
Notice how our test suite is developing? We went from hard-coding props inside their own test cases to creating a factory for them.
请注意我们的测试套件是如何开发的? 我们从在他们自己的测试用例中对道具进行硬编码到为他们创建工厂。
安排,行动,断言 (Arrange, Act, Assert)
This following integration test can be broken into three parts: arrange, act, and assert.
下面的集成测试可以分为三个部分:安排,执行和声明。
Arrange: create props and other fixtures for the test case
安排:为测试用例创建道具和其他固定装置
Act: simulate changes to the elements such as text inputs or button clicks
行动:模拟对元素的更改,例如文本输入或按钮单击
Assert: assert that the desired functions were invoked the right number of times, and with the correct arguments
断言:断言所需的函数被调用了正确的次数并带有正确的参数
There are some assumptions made about the code, like the naming of our labels or the fact that we will have a createComment
prop.
对代码有一些假设,例如标签的命名或我们将拥有createComment
道具的事实。
When finding inputs, we want to try to find them by their labels. This prioritizes accessibility when we’re building our applications. The easiest way to grab the form is by using container.querySelector
.
查找输入时,我们希望尝试按其标签查找它们。 在构建应用程序时,这优先考虑了可访问性。 获取表单的最简单方法是使用container.querySelector
。
Next, we must assign new values to the inputs and simulate the change to update their state. This step may feel a little strange, since normally we type one character at a time, updating the component’s state for each new character.
接下来,我们必须为输入分配新值,并模拟更改以更新其状态。 这一步可能会有些奇怪,因为通常我们一次键入一个字符,并为每个新字符更新组件的状态。
This test behaves more like the behavior of copy/paste, going from empty string to ‘Socrates’. No breaking issues for now, but we may want to make note of that in case it comes up later.
该测试的行为更像复制/粘贴的行为,从空字符串变为“ Socrates”。 目前尚无重大问题,但我们可能要注意一下,以防稍后出现。
After submitting the form, we can make assertions on things like which props were invoked and with what arguments. We could also use this moment to verify that the form inputs cleared.
提交表单后,我们可以断言诸如调用了哪些道具以及带有哪些参数的事情。 我们也可以利用这一时刻来验证是否清除了表单输入。
Is it intimidating? No need to fear, my child, walk this way. Start by adding the form to your render function.
令人生畏吗? 我的孩子,不用担心,走这条路。 首先将表单添加到渲染函数中。
I could break this form into its own separate component, but I will refrain for now. Instead, I’ll add it to my “Refactor Wish List” I keep beside my desk.
我可以将此表格分解为自己的单独部分,但现在不再赘述。 相反,我将其添加到我的办公桌旁的“重构愿望清单”中。
This is the way of TDD. When something seems like it can be refactored, make a note of it and move on. Refactor only when the presence of an abstraction benefits you and doesn’t feel unnecessary.
这就是TDD的方式。 当某些东西似乎可以重构时,记下它并继续前进。 仅当抽象的存在使您受益并且觉得不必要时才进行重构。
Remember when we refactored our test suite by creating the createProps
factory? Just like that. We can refactor tests, too.
还记得我们通过创建createProps
工厂重构测试套件时的情况吗? 就这样 我们也可以重构测试。
Now, let’s add in the handleChange
and handleSubmit
class methods. These get fired when we change an input or submit our form. I will also initialize our state.
现在,让我们添加handleChange
和handleSubmit
类方法。 当我们更改输入或提交表单时,这些将被解雇。 我还将初始化我们的状态。
And that did it. Our tests are passing and we have something that sort of resembles a real application. How does our coverage look?
做到了。 我们的测试通过了,我们有些类似于真实的应用程序。 我们的保险范围如何?
Not bad. If we ignore all of the setups that go inside index.js, we have a fully covered web application with respect to lines executed.
不错。 如果我们忽略index.js内的所有设置,那么就执行的行而言,我们将有一个覆盖全面的Web应用程序。
Of course, there are probably other cases we want to test in order to verify that the application is working as we intend. That coverage number is just something your boss can brag about when they’re talking to the other cohorts.
当然,可能还需要测试其他情况,以验证应用程序是否按预期工作。 这个覆盖率数字只是您的老板在与其他同类交谈时可以吹嘘的东西。
喜欢评论 (Liking Comments)
How about we check that we can like a comment? This may be a good time to establish some concept of authentication within our application. But we’ll not jump too far just yet. Let’s first update our props factory to add an auth
field along with IDs for the comments we generate.
我们如何检查我们是否喜欢评论? 这可能是在我们的应用程序中建立身份验证概念的好时机。 但是我们还不会跳得太远。 让我们首先更新道具工厂,为我们生成的注释添加一个auth
字段以及ID。
The user who is “authenticated” will have their auth
property passed down through the application. Any actions that are relevant to whether they are authenticated will be noted.
被“认证”的用户将通过应用程序传递其auth
属性。 与它们是否经过验证有关的任何动作都将被记录下来。
In many applications, this property may contain some sort of access token or cookie that is sent up when making requests to the server.
在许多应用程序中,此属性可能包含某种访问令牌或cookie,这些令牌是在向服务器发出请求时发送的。
On the client, the presence of this property lets the application know that they can let the user view their profile or other protected routes.
在客户端上,此属性的存在使应用程序知道他们可以让用户查看其配置文件或其他受保护的路由。
In this testing example, however, we will not fiddle too hard with authentication. Imagine a scenario like this: When you enter a chatroom, you give your screen name. From that point on, you are in charge of every comment that uses this screen name, despite who else signed in with that name.
但是,在此测试示例中,我们不会花太多时间进行身份验证。 想象一下这样的情况:进入聊天室时,输入屏幕名称。 从那时起,您将负责使用该屏幕名称的所有评论,尽管还有其他人使用该名称登录。
While it is not a great solution, even in this contrived example, we are only concerned with testing that the CommentFeed component behaves as it should. We are not concerned with how our users are logged in.
尽管这不是一个很好的解决方案,但即使在这个人为的示例中,我们也只关心测试CommentFeed组件的行为是否正常。 我们不关心我们的用户如何登录。
In other words, we may have a totally different login component that handles the authentication of a particular user, thus sending them through hoops of fire and fury in order to derive the almighty auth
property that lets them wreak havoc in our application.
换句话说,我们可能拥有一个完全不同的登录组件来处理特定用户的身份验证,从而通过大火暴发的方式将他们发送出去,以派生全能的auth
属性,从而使他们在我们的应用程序中遭受严重破坏。
Let’s “like” a comment. Add this next test case and then update the props factory to include likeComment
.
让我们“喜欢”评论。 添加下一个测试用例,然后将props工厂更新为包括likeComment
。
And now for the implementation, we’ll start by updating the Comment component to have a like button as well as a data-testid
attribute so we can locate it.
现在,对于实现,我们将从更新Comment组件开始,以使其具有一个like按钮以及一个data-testid
属性,以便我们能够找到它。
I put the test ID directly on the button so that we can immediately simulate a click on it without having to nest query selectors. I also attached an onClick
handler to the button so that it calls the onLike
function passed down to it.
我将测试ID直接放在按钮上,这样我们就可以立即模拟对其的单击,而不必嵌套查询选择器。 我还在按钮上附加了onClick
处理程序,以便它调用传递给它的onLike
函数。
Now we just add this class method to our CommentFeed:
现在,我们将此类方法添加到CommentFeed中:
You may wonder why we don’t simply pass the likeComment
prop directly to the Comment component. Why do we make it a class property?
您可能想知道为什么我们不直接将likeComment
直接传递给Comment组件。 我们为什么要使其成为阶级财产?
In this case, because it is rather simple, we don’t have to build this abstraction. In the future, we may decide to add other onClick
handlers that, for example, handle analytics events or initiate a subscription to that post’s future comments.
在这种情况下,因为它很简单,所以我们不必构建这种抽象。 将来,我们可能会决定添加其他onClick
处理程序,例如,处理分析事件或启动对该帖子的未来评论的订阅。
Being able to bundle multiple different function calls in the handleLike
method of this container component has its advantages. We could also use this method to update the state of the component after a successful “Like” if we so choose.
能够在此容器组件的handleLike
方法中捆绑多个不同的函数调用具有其优点。 如果愿意,我们也可以使用此方法在成功完成“赞”后更新组件的状态。
不喜欢评论 (Disliking Comments)
At this point we have working tests for rendering, creating, and liking comments. Of course, we haven’t implemented the logic that actually does that — we’re not updating the store or writing to a database.
至此,我们已经有了用于渲染,创建和喜欢注释的工作测试。 当然,我们尚未实现实际执行此操作的逻辑-我们不会更新存储或写入数据库。
You might also notice that the logic we’re testing is fragile and not terribly applicable to a real-world comment feed. For example, what if we tried to like a comment we already liked? Will it increment the likes count indefinitely, or will it unlike it? Can I like my own comments?
您可能还会注意到,我们正在测试的逻辑是脆弱的,不适用于现实世界的评论供稿。 例如,如果我们想喜欢我们已经喜欢的评论该怎么办? 它会无限期增加点赞次数,还是会与点赞次数不同? 我可以喜欢我自己的评论吗?
I’ll leave extending the functionality of the components to your imagination, but a good start would be to write a new test case. Here’s one that builds off the assumption that we would like to implement disliking a comment we already liked:
我将把组件的功能扩展到您的想象中,但是一个好的开始是编写一个新的测试用例。 这是在我们想实施不喜欢我们已经喜欢的评论的假设的基础上建立的:
Notice that this comment feed we’re building allows me to like my own comments. Who does that?
请注意,我们正在构建的此评论提要使我喜欢自己的评论。 是谁啊
I have updated the Comment component with some logic to determine whether or not the current user has liked the comment.
我已使用一些逻辑更新了Comment组件,以确定当前用户是否喜欢该评论。
Well I cheated a little bit: where we were passing author
to the onLike
function before, I changed to currentUser
, which is the auth
prop passed down to the Comment component.
好吧,我作弊了一点:在我们之前将author
传递给onLike
函数的地方,我更改为currentUser
,这是传递给Comment组件的auth
道具。
After all, it wouldn’t make sense for the comment’s author to show up when someone else likes their comment.
毕竟,当其他人喜欢他们的评论时,让评论的作者出现是没有意义的。
I realized this because I was vigorously writing tests. Had I just been coding by coincidence this might’ve slipped past me until one of my coworkers berated me for my ignorance!
我意识到这一点是因为我正在积极编写测试。 如果我只是在巧合地编码,那么它可能已经溜走了,直到我的一位同事因我的无知而指责我!
But there is no ignorance here, just tests and the code that follows. Be sure to update the CommentFeed so that it expects to pass down the auth
property. For the onClick
handlers we can omit passing around the auth
property, since we can derive that from the auth
property in the parent’s handleLike
and handleDislike
methods.
但是这里没有愚昧,只有测试和随后的代码。 确保更新CommentFeed,以便它期望传递auth
属性。 对于onClick
处理程序,我们可以忽略传递auth
属性,因为我们可以从父级的handleLike
和handleDislike
方法中的auth
属性派生该属性。
结语 (Wrapping up)
Hopefully, your test suite is looking like an unlit Christmas tree.
希望您的测试套件看起来像一棵熄灭的圣诞树。
There are so many different routes we can take at this, it can get a little overwhelming. Every time you get an idea for something, just write it down, either on paper or in a new test block.
我们可以采取许多不同的路线,这可能会使您有些不知所措。 每当您对某事有想法时,只需将其写下来,无论是在纸上还是在新的测试块中。
For example, say you actually want to implement handleLike
and handleDislike
in one single class method, but you have other priorities right now. You can do this by documenting in a test case like so:
例如,假设您实际上想在一个单一的类方法中实现handleLike
和handleDislike
,但现在您还有其他优先级。 您可以通过在测试用例中进行记录来做到这一点,如下所示:
This doesn’t mean you need to write an entirely new test. You could also update the previous two cases. But the point is, you can use your test runner as a more imperative “To Do” list for your application.
这并不意味着您需要编写一个全新的测试。 您还可以更新前两种情况。 但关键是,您可以将测试运行程序用作应用程序中更重要的“待办事项”列表。
有用的网址 (Helpful Links)
There are a few great pieces of content out there that deal with testing at large. Here are some in particular that inspired this article as well as my own practices.
那里有一些很棒的内容可以处理整个测试。 特别是一些启发本文和我自己的实践的东西。
“Introducing the React Testing Library" by Kent C. Dodds. It’s a good idea to understand the philosophy behind this testing library.
Kent C. Dodds的 “ 介绍React测试库 ”是理解此测试库背后的哲学的一个好主意。
“Software Testing Anti-patterns" by Kostis Kapelonis. An extremely in-depth article that discusses unit and integration testing. Also how not to do them.
Kostis Kapelonis撰写的“ Software Testing Anti-patterns ”,一篇非常深入的文章,讨论了单元和集成测试,以及如何不进行测试。
“Test Driven Development by Example" by Kent Beck. This is a physical book that discusses TDD patterns. It is not too long and it is written conversationally, making it easy to digest.
肯特·贝克(Kent Beck)撰写的“ 通过示例进行测试驱动的开发 ”是一本讨论TDD模式的物理书籍,时间不长,而且是通过对话编写的,因此易于理解。
I hope that’ll tide you over for a while.
我希望这能帮你一会儿。
Curious for more posts or witty remarks? If you enjoyed this article, give me some claps and follow me on Medium, Github, and Twitter!
对更多帖子或机智的言论感到好奇吗? 如果您喜欢这篇文章,请给我一些鼓掌,并在Medium , Github和Twitter上关注我!
tdd 使用详解