一月蔷薇_456 发表于 2018-2-27 15:09:15

聊聊如何写单元测试

这篇文章的假设为你明确自己要写单元测试了,如果您不符合这个假设,可以参看这篇文章 先解决
思想:为何要写单元测试

当打算开始写单元测试时。你调整了下坐姿,气运丹田,感觉到冥冥之中又向高质量代码迈进了一
步,但当你的手下意识的敲击键盘的时候,又觉得似乎哪里不太对劲:“恩,应该怎样开始写一个单
元测试呢?”

如何写单元测试

首先我们需要明确,什么叫做单元测试。

在计算机编程中,单元测试(英语: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,毕竟人无完人。

从入门到放弃?

相信很多朋友其实也写过单测,但或因需求变更过快,或因一次次的失败无力解决,导致了
最终没有坚持下来。这当中其实是有一定技巧的,使用良好的技巧会在保证测试覆盖率的
同时,降低测试失败的频率。下次就来说说 如何使用一些技巧,让我们容易坚持执行这个
应该坚持的单元测试。

海海豚 发表于 2018-2-27 15:40:03

谢谢分享,对单元测试有了一个更深入的了解

梦想家 发表于 2018-2-28 11:02:19

:handshake
页: [1]
查看完整版本: 聊聊如何写单元测试