聊聊如何写单元测试
这篇文章的假设为你明确自己要写单元测试了,如果您不符合这个假设,可以参看这篇文章 先解决思想:为何要写单元测试
当打算开始写单元测试时。你调整了下坐姿,气运丹田,感觉到冥冥之中又向高质量代码迈进了一
步,但当你的手下意识的敲击键盘的时候,又觉得似乎哪里不太对劲:“恩,应该怎样开始写一个单
元测试呢?”
如何写单元测试
首先我们需要明确,什么叫做单元测试。
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的
最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。
我的理解是:测试某个具体的函数,是否符合编写者的预期。
其实也很好理解,就是将你编写某个函数的功能与你的预期做一个比较,如果函数运行的结果与你
的预期相符,则说明测试通过,反之则失败。
举个很简单的例子
class Person
attr_accessor :name, :gender
def initialize(name, gender)
self.name = name
self.gender = gender
end
def say_hello
puts "#{self.title} #{name} said hello."
end
def title
self.gender == "male" ? "Mr" : "Ms"
end
end我想测试一下 title 这个函数是否符合我预期,于是我会这样写测试(假设使用Rails 原生的 test框架)
require 'test_helper'
class PersonTest < ActiveSupport::TestCase
test "should return title correctly" do
person = Person.new("Ji Cheng", "Male")
assert_equal "Mr", person.title
person = Person.new "Han Meimei", "Female"
assert_equal "Ms", person.title
end
end语言不同,测试框架不同都会导致代码不同,但是思想都是一样的,都是去 assert 一个值,与运行
后的函数值保持一致。
也许有人会说你这个函数太简单了,简直不用看就知道会发生什么,为什么还要写个测试? 其实想
想,写出这个测试也花多长时间,更重要的是,你在写这个测试的时候,会更加清楚这个函数输入
与输出,是否满足预期,以及多一次使用自己写的函数的机会,体谅下调用你写的函数的人。
细心的朋友肯定已经看出来了,这个测试是会报错的,如果你真没看出来,就更加说明单元测试的
重要性。实际情况中太多函数是看不出来的,但是例如下面的 analysis_message
class JobStepService
attr_accessor :project, :job, :flow
# Some functions ...
class << self
def analysis_message(hash)
job_id = hash[:job_id]
index = hash[:index]
job_step = JobStep.find_by(job_id: job_id, index: index)
return if job_step.nil? || job_step.status == "stopped"
try_mark_last_job_status(job_id, index)
where = JobStep.where(job_id: job_id, index: index)
safe_update_job_hash(where, hash)
job_step.reload
end
private
def try_mark_last_job_status(job_id, index)
return if index.to_i.zero?
# 必须得找到, 不找到肯定是哪里出错了,应该抛异常
JobStep.find_by(job_id: job_id, index: index.to_i - 1).update_attribute(:status, "success")
end
end
end
当不是那么容易看出的时候,去写一个单元测试是跟你在命令端调试所花的时间是差不
想必大家也看出来了,测试甚至有些随意不太友好,但是至少在跑了这段测试之后,我很信任之前
写的函数是没有问题的(就算有,也不会是那些会被同事耻笑的低级错误)。
写单元测试一些实践
大前提
所有的实践前面都有一个大前提:首先你得写单元测试。我非常喜欢写一些显而易见的单元测试当
做休息放松,当别人问我为什么写这种测试的时我通常是以“增加代码测试覆盖率”来忽悠他们。
(但是实际上还是有30%左右的概率会测出各种问题,包含各种语法错误,误触某回调等奇怪的错
误校验不过,也许我就是一个粗心的人),这样做还有另外一个好处,培养自己对每个方法都写测
试的习惯:连很简单的方法都写了,那稍微复杂点的,简直不能忍。
谁来写
开发来写。单测主要测试的是具体的函数,没有比开发人员更熟悉自己写的函数了,同时本着“吃
自己的狗粮”的原则,也可以反省下自己设计的函数是否合理。最重要的,当自己写完一个的时候,
就可以把单元测试当做自己手动调试代码,这样就可以很自然的无缝的将单测写上,而不用等测
试人员排队做。
关于测试覆盖率
虽然这个东西听起来很虚,但我觉得是个必需品,必须得上。当有一个标准去衡量自己的工作进
度的时候,潜意识中大家会努力的提高这个指标。同时绝大多数测试覆盖率统计工具,都能通过
界面显示出你函数中未覆盖的逻辑,避免自己漏测。我自己使用simplecov 这个gem 来统计我自
己的Rails 项目的测试覆盖。
写的测试跑着要快
我非常赞同,写的测试越慢,由于人的惰性,会导致自己因为不想等太久而不跑测试。测试写的
再多,不跑全是白搭。
其实一个单元测试的内容很少,那么一般慢是慢在哪里呢?
我觉得有以下方面
IO
sleep/wait 语句
数据库的大量写入
关于IO
目前我遇见比较多的是关于网络的IO, 有些第三方组件会接入网络,这种一般都会带来500ms
左右的延时,运气不好连国外(比如我们的项目连github API)没准就会变成假摔(一定概率的
跑出错,非必现的错误)。常见的操作是 Stub 解决问题,各大语言都有很成熟的解决方案。比
如我现在使用的 webmock 这个 gem ,当然以ruby 这种 “开放式” 语言的能力,就算不引入任何
gem,写个猴子补丁也会非常的轻松。
关于sleep/ wait
大多数使用sleep/ wait 的时候都是在等待某个异步方法的执行完成,我的处理方式是将异步的处
理以及等待后面的语句都抽成两个独立的函数,分别测试这两个函数,从而避免走sleep 这种慢
的操作
关于数据库
很多测试相关的文章和书籍都强调 数据库太慢了,所以不能使用数据库。我不太认同,因为其
实很多时候写的代码都需要依赖数据库的一些特性,或者离开数据库而存在内存中会很麻烦(
比如查询语句,脱离数据库mock 个 where 很麻烦)。我的策略(当然是Rails 的策略)是使用
专门用于测试的数据库,每当运行一个单侧的时候就会把它清除掉。这样,测试数据库的数据
会非常的少,查询、新增起来大多数情况下其实也在20ms以内。
我是非常反对当一个单元测试跑完后,不清除数据库的,可能这些数据会影响到其他单元测试,
进一步造成了测试的假摔,假摔是大忌,应该尽量避免。当然清数据库也不是绝对的,需要自
己灵活判别,比如下面的情况。
我运行测试之前会生成100条左右的模板数据,这些数据是我在进行单元测试的时候绝对不会
操作的,所以没必要每次执行一个单元测试删除再新建。但是为了防止我自己有时候没想清楚
改掉模板数据从而有可能造成假摔,所以我在执行每个单元测试之前会判断下这些模板数据的
行数是否是我最初的生成的行数。
单元测试不是万能的
会有人觉得我花了那么大的功夫,覆盖率90%了,上 jenkins 或者 flow.ci 了,那我的程序就
很稳定了。这种观念当然是不对的,就如同你买了一把200块的锁就指望自己的自行车永远
不会被偷一样。良好的单元测试会极大的提高程序稳定性,但是不会百分百的保证程序一
定ok,毕竟人无完人。
从入门到放弃?
相信很多朋友其实也写过单测,但或因需求变更过快,或因一次次的失败无力解决,导致了
最终没有坚持下来。这当中其实是有一定技巧的,使用良好的技巧会在保证测试覆盖率的
同时,降低测试失败的频率。下次就来说说 如何使用一些技巧,让我们容易坚持执行这个
应该坚持的单元测试。
谢谢分享,对单元测试有了一个更深入的了解
:handshake
页:
[1]