测试程序使用什么语言编写好
When practicing test driven development (TDD), we sometimes tend to focus on testing everything. This 100% coverage mentality can sometimes lead us to overcomplicate things.
在实践测试驱动开发(TDD)时,我们有时倾向于专注于测试所有内容。 这种100%的覆盖心态有时会导致我们过于复杂。
Before, I was the one leading the charge to make tests DRY-er, because I hated seeing repetitive code. I was new to metaprogramming in Ruby back then, and I always wanted to make things “simpler” by mashing up the repetitive code and coming up with a monster. Case in point:
以前,我是负责测试DRY-er的负责人,因为我讨厌看到重复的代码。 那时我还不熟悉Ruby中的元编程,我一直想通过混搭重复的代码并提出一个怪兽来使事情“更简单”。 例子:
describe 'when receiving the hero details' do
it 'should have the top level keys as methods' do
top_level_keys = %w{id name gender level paragonLevel hardcore skills items followers stats kills progress dead last-updated}
top_level_keys.each do |tl_key|
@my_hero.send(tl_key).must_equal @my_hero.response[tl_key.camelize(:lower)]
end
end
Okay, so that was way back in 2012. As a background, this was a Ruby gem (similar to an npm package) for Blizzard’s Diablo 3 API. So what was I testing here? Upon reading the code, it seems pretty straightforward: it says the top level keys can be methods. So if the API was returning something like:
好的,那可以追溯到2012年。作为背景,这是暴雪的Diablo 3 API的Ruby宝石 (类似于npm软件包 )。 那我在这里测试什么呢? 阅读代码后,它看起来非常简单:它说顶级键可以是方法。 因此,如果API返回如下内容:
{
paragonLevel: 10,
hardcore: true,
kills: 1234
}
Then given a hero instance, I can just call the them as methods and it should return them like so:
然后给定一个hero实例,我可以将它们作为方法调用,它应该像这样返回它们:
> hero = Covetous::Profile::Hero.new 'user#1234', '1234'
> hero.paragon_level # 10
> hero.kills # 1234
Okay, I’ll be honest. When I was writing this article, I was looking through my old open source projects as an example and saw this. It looked pretty straightforward as I said, but while actually analyzing it, I realised it was much worse than I thought. It took me fifteen minutes to just get what it does, even if the spec says what it should be doing. Before I typed the above block, I wanted to double check I understood it correctly. While I did, the way I wrote the tests made everything confusing. Why?
好吧,我会说实话。 在撰写本文时,我以旧的开源项目为例进行了研究并看到了这一点。 正如我说的那样,它看起来非常简单,但是在进行实际分析时,我意识到它比我想象的要糟糕得多。 我花了十五分钟才知道它应该做什么,即使该规范说了应该做什么。 在输入上面的代码块之前,我想仔细检查一下我是否正确理解它。 当我这样做的时候,我编写测试的方式使一切变得混乱。 为什么?
问题 (The problem)
As I said, I was new to metaprogramming back then and saw an opportunity to use it. At that time, it seemed very clever, but now that I have more experience, I know that doing this in tests is a liability more than a boon.
就像我说的那样,我那时对元编程还是陌生的,并且看到了使用它的机会。 那时,这似乎很聪明,但是现在我有了更多的经验,我知道在测试中这样做比承担更多的责任更大。
You see, one of the things I learned is that test code is untested code. Let that sink in for a bit.
您知道,我学到的一件事是测试代码是未经测试的代码 。 让它沉入一点。
Those are two different links by the way. It basically means that ANY code that your test runs can potentially have it’s own errors. You don’t actually have tests for test code, so there’s no guarantee that it works. The only guarantee you can do is have the test fail when you comment out the actual lines in the code and have it pass when you uncomment it. Sometimes though, even with this red-green testing, you can still get false positives. So the best way to avoid this is to keep your tests as simple as possible and as explicit as possible.
顺便说一下,这些是两个不同的链接。 从根本上讲,这意味着您测试运行的任何代码都可能有其自身的错误。 您实际上没有测试代码的测试,因此无法保证它能正常工作。 唯一可以做的保证是,在注释掉代码中的实际行时使测试失败,而在取消注释时使测试通过。 尽管有时,即使进行了这种红绿色测试,您仍然可以得到误报。 因此,避免这种情况的最佳方法是保持测试尽可能简单和明确 。
So back to my test. If I remember correctly, I initially did the methods one by one. I then saw a pattern which made me think that it was going to be the same pattern for all the methods at least, so why not make the code DRY-er?
回到我的测试。 如果我没记错的话,我最初是一一完成的。 然后,我看到了一个模式,使我认为至少对于所有方法而言,它都将是同一模式,那么为什么不使代码DRY -er?
I made an array of all the possible methods, looped through them, and did an assertion that calling the method should be the same as looking at the response and getting the value. Easy enough, but the main thing that put me off was this:
我对所有可能的方法进行了数组排列,遍历了它们,并断言调用该方法应与查看响应并获取值相同。 这很容易,但是让我失望的主要是:
@my_hero.send(tl_key).must_equal @my_hero.response[tl_key.camelize(:lower)]
In Ruby, send
calls the passed string as a method. So if tl_key
's value was paragonLevel
(from the array), this line basically says:
在Ruby中, send
传递的字符串作为方法进行调用。 因此,如果tl_key
的值是paragonLevel
(来自数组),则此行基本上说:
@my_hero.paragonLevel.must_equal @my_hero.response['paragonLevel']
See, this is where I keep doubting myself again. My README
says it should be @my_hero.paragon_level
, but looking at the test, it isn’t. Who should I trust now? My tests that are passing, or my README
? This is the exact reason why metaprogramming in tests is dangerous — you never truly know if your tests are passing, either because they are correct or you misconfigured it somehow. It’s almost the same as NOT writing tests!
瞧,这就是我再次怀疑自己的地方。 我的README
说应该为@my_hero.paragon_level
,但从测试来看,事实并非如此。 我现在应该信任谁? 我通过的测试还是README
? 这就是测试中的元编程之所以危险的确切原因-您永远无法真正知道测试是否通过,要么是因为测试正确,要么是您以某种方式配置了错误。 几乎与不编写测试一样!
做一个更好的方法 (Doing it a better way)
So how would I re-write this? I have since learned that writing tests for my ten year old self would suffice. Meaning, myself ten years ago. I always ask myself: “Ten years from now, would I still be able to understand this, without context?” If not, then that means I either need to write a note in the comments or my test is too complicated.
那么我该如何重写呢? 从那以后,我了解到为自己十岁的自我编写测试就足够了。 意思是,我十年前。 我总是问自己:“十年后,我仍然可以在没有上下文的情况下理解这一点吗?” 如果不是,则意味着我要么需要在注释中写注释, 要么我的测试太复杂。
Let’s try re-writing this. As I said, we should be as simple and as explicit as possible. Here’s one solution:
让我们尝试重写它。 正如我所说,我们应该尽可能简单明了。 这是一种解决方案:
# Given I queried my hero against the API:
let(:my_hero) { Covetous::Profile::Hero.new 'corroded-6950', '12345678' }
it 'should have the top level keys as methods' do
expect(my_hero.id).to eq 12345
expect(my_hero.name).to eq 'corrodeath'
expect(my_hero.gender).to eq 'female'
expect(my_hero.level).to eq 70
...
end
See how explicit it is? It’s repetitive, sure, but 10 years from now I am pretty sure I would still understand what my expectations were. I don’t have to ‘compile and interpret’ the code in my brain. I just read the specs!
看看它有多明确? 这是重复的,当然可以,但是从现在开始的十年后,我很确定自己仍然会明白我的期望。 我不必在大脑中“编译和解释”代码。 我只是看了规格!
Also, with this, I didn’t even have to recall what camelize(:lower)
actually does (confession: I had to look it up while I was reading through my old code).
而且,有了这个,我什至不必回想一下camelize(:lower)
实际作用(坦白:在阅读旧代码时,我不得不camelize(:lower)
一下它)。
How about another example? So given we have a model:
另一个例子呢? 因此,给定我们有一个模型:
class Something < ActiveRecord::Base
VALID_THINGS = %w(yolo swag)
OTHER_VALID_THINGS = %w(thing another_thing)
def valid_things_ids
where(group: group).pluck(:id)
end
end
The above is just a contrived example based off a real class we have in my current company. The spec I saw was this:
上面只是一个基于我们现有公司的真实课程的人为设计示例。 我看到的规格是这样的:
subject(:valid_things_ids) { described_class.valid_things_ids(group) }
let(:group) { 'example' }
before do
described_class::VALID_THINGS.each do |thing|
FactoryGirl.create(:something, group: 'example', name: thing)
end
end
described_class::VALID_THINGS.each do |thing|
it "contains things with the name #{thing}" do
the_thing = described_class.find_by_group_and_name('example', thing)
expect(valid_things_ids).to include the_thing.id
end
end
Okay. First, this is a correct test, in that given a number of somethings
, we can call the method and it returns us all the ids of somethings
with that group (e.g. example
).
好的。 首先,这是一个正确的测试,因为给定了一些somethings
,我们可以调用该方法,它会向我们返回该组中somethings
所有ID(例如example
)。
My issue with this, however, is do we need to test all the valid things? What about OTHER_VALID_THINGS
? If we want to test all the possible values of VALID_THINGS
, then we should also test all the possible values of OTHER_VALID_THINGS
. If we DON’T want to test all possible values, then why use VALID_THINGS
? Why not just contrive a random sample and just prove that the method works?
但是,我的问题是,我们需要测试所有有效的东西吗? OTHER_VALID_THINGS
呢? 如果我们要测试VALID_THINGS
所有可能值,那么我们还应该测试OTHER_VALID_THINGS
所有可能值。 如果我们不想测试所有可能的值,那么为什么要使用VALID_THINGS
? 为什么不只是设计一个随机样本并仅仅证明该方法有效呢?
How about something like this?
这样的事情怎么样?
subject(:valid_things_ids) { described_class.valid_things_ids(group) }
let(:group) { 'blurb' }
let!(:random_thing) { FactoryGirl.create(:something, group: 'blurb', id: 111) }
let!(:another_thing) { FactoryGirl.create(:something, group: 'blurb', id: 222) }
let!(:not_included) { FactoryGirl.create(:something, group: 'shrug', id: 333) }
it do
expect(valid_things_ids).to include 111
expect(valid_things_ids).to include 222
expect(valid_things_ids).not_to include 333
end
So here, I create 3 somethings
and give them ids. I make the third one have a different group. Now if I run the method with blurb
as the argument, I can expect that it includes the first two and not the last one.
所以在这里,我创建3个somethings
并给他们id。 我让第三个有不同的组。 现在,如果我使用blurb
作为参数来运行该方法,则可以预期它包括前两个而不是最后一个。
Reading it a few months from now, I won’t be confused as to what is being tested since it is straightforward, and I don’t even have to ask why I am only testing a certain part of the code and not all.
从现在开始阅读几个月后,我不会对正在测试的内容感到困惑,因为它很简单,我什至不必问为什么我只测试代码的一部分而不是全部。
Also take note of the explicitness of the test. I am expecting it to include the ids 111
and 222
. Normally, people would test it like so:
还要注意测试的明确性。 我希望它包括id 111
和222
。 通常,人们会像这样测试它:
expect(valid_things_ids).to include random_thing.id
I don’t really like these tests, because they still rely on the code at this point. If for some reason the id is nil
, and the code also had a bug where it returned nil
, then this test would still pass. Not with explicit ids and expectations, though. Of course there will be caveats, but I think I would like to deal with those rather than the uncertainty of possible false positives.
我真的不喜欢这些测试,因为此时它们仍然依赖于代码。 如果由于某种原因id为nil
,并且代码还存在返回nil
的错误,则该测试仍将通过。 但是,没有明确的ID和期望。 当然会有警告,但是我想我要处理这些警告,而不是可能出现误报的不确定性。
结语 (Wrapping up)
As you can see from both the examples above, simple to read tests will help you in the long run. Being very explicit helps a lot in understanding tests and having fewer bugs.
从以上两个示例中都可以看到,简单易懂的测试从长远来看将为您提供帮助。 明确表示有助于理解测试,并减少错误。
Remember, your 100% test coverage won’t matter if half of those are false positives. Always remember your past self when testing. Try to think far ahead into the future and ask yourself what your tests mean.
请记住,如果其中一半是误报,则100%的测试覆盖率将无关紧要。 测试时,请记住过去的自我。 尝试思考未来,并问自己测试的意义。
测试程序使用什么语言编写好