一起来聊聊如何写单元测试吧

发表于:2022-9-22 10:28

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:atpking    来源:简书

  这篇文章主要讨论一下几点问题:
  ·如何开始写一个单元测试
  · 单元测试有我自己的一些实践
  当打算开始写单元测试时。你调整了下坐姿,气运丹田,感觉到冥冥之中又向高质量代码迈进了一步,但当你的手下意识的敲击键盘的时候,又觉得似乎哪里不太对劲:“恩,应该怎样开始写一个单元测试呢?”
  如何写单元测试
  首先我们需要明确,什么叫做单元测试。
  在计算机编程中,单元测试(英语: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;
  # encoding: utf-8
  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
  当不是那么容易看出的时候,去写一个单元测试是跟你在命令端调试所花的时间是差不多的。
  class JobStepTest < ActiveSupport::TestCase
    setup do
      # do some initialize work...
    end
    test "could analysis message correctly" do
      JobStepService.new(@job).generate_job_steps
      assert_equal false, JobStep.count == 1
      hash = { index: 0, status: "success", return_value: 0, log: "hello world\n", job_id: @job.id.to_s, category: "step" }
      JobStepService.analysis_message hash
      assert_equal "success", JobStep.asc(:index).first.status
      assert_equal "pending", JobStep.asc(:index).last.status
      hash = { index: 2, status: "failure", return_value: 0, log: "hello world\n", job_id: @job.id.to_s, category: "step" }
      JobStepService.analysis_message hash
      assert_equal "success", JobStep.asc(:index).to_a[1].status
      assert_equal "failure", JobStep.asc(:index).to_a[2].status
    end
  想必大家也看出来了,测试甚至有些随意不太友好,但是至少在跑了这段测试之后,我很信任之前写的函数是没有问题的(就算有,也不会是那些会被同事耻笑的低级错误)。
  写单元测试一些实践
  大前提
  所有的实践前面都有一个大前提:首先你得写单元测试。我非常喜欢写一些显而易见的单元测试当做休息放松,当别人问我为什么写这种测试的时我通常是以“增加代码测试覆盖率”来忽悠他们。(但是实际上还是有30%左右的概率会测出各种问题,包含各种语法错误,误触某回调等奇怪的错误校验不过,也许我就是一个粗心的人),这样做还有另外一个好处,培养自己对每个方法都写测试的习惯:连很简单的方法都写了,那稍微复杂点的,简直不能忍。
  谁来写
  开发来写。单测主要测试的是具体的函数,没有比开发人员更熟悉自己写的函数了,同时本着“吃自己的狗粮”的原则,也可以反省下自己设计的函数是否合理。最重要的,当自己写完一个的时候,就可以把单元测试当做自己手动调试代码,这样就可以很自然的无缝的将单测写上,而不用等测试人员排队做。
  关于测试覆盖率
  虽然这个东西听起来很虚,但我觉得是个必需品,必须得上。当有一个标准去衡量自己的工作进度的时候,潜意识中大家会努力的提高这个指标。同时绝大多数测试覆盖率统计工具,都能通过界面显示出你函数中未覆盖的逻辑,避免自己漏测。我自己使用simplecov 这个gem 来统计我自己的Rails 项目的测试覆盖。
  写的测试跑着要快
  我非常赞同,写的测试越慢,由于人的惰性,会导致自己因为不想等太久而不跑测试。测试写的再多,不跑全是白搭。
  其实一个单元测试的内容很少,那么一般慢是慢在哪里呢?
  我觉得有以下方面:
  1、IO
  2、sleep/wait 语句
  3、数据库的大量写入
  关于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,毕竟人无完人。
  从入门到放弃?
  相信很多朋友其实也写过单测,但或因需求变更过快,或因一次次的失败无力解决,导致了最终没有坚持下来。这当中其实是有一定技巧的,使用良好的技巧会在保证测试覆盖率的同时,降低测试失败的频率。下次就来说说 如何使用一些技巧,让我们容易坚持执行这个应该坚持的单元测试。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号