前面介绍了单元测试的框架nunit,它可以很好的帮助我们建立测试,检验我们的代码是否正确。但这还不够,有时候我们的业务比较重,会依赖其它的类。基于隔离测试的原则,我们不希望依赖的其它类影响到我们的测试目标。这时候mock就显得十分重要了。当然还有其它因素使得我们必须mock对象,比如配置文件,db等。

提供mock技术的工具很多:moq,nsubstitute,rhinomocks,typemock,justmock等。开源免费的工具功能局限,像moq, 的博客写得很好。这里我选择justmock,付费版本可以使用高级功能。

justmock 开始

安装 justmock ,从官网,默认安装。

添加 telerik.justmock.dll 引用,在安装目录下,默认为:c:\program files (x86)\progress\telerik justmock\libraries 。

开启使用高级功能

为什么需要mock

先看我们需要测试的一个方法:

        /// <summary>
        /// 转账
        /// </summary>
        /// <param name="accounta"></param>
        /// <param name="accountb"></param>
        /// <param name="money"></param>
        /// <returns></returns>
        public double transferaccounts(bankaccount accounta, bankaccount accountb, double money)
        {
            double transferlimit = 50000.0;//转账最高限制
            try
            {
                var balancea = accounta.drawmoney(money);
                accountb.savemoney(money);
                return balancea;
            }
            catch (exception ex)
            {
                throw new exception($"转账失败,{ex.message}");
            }
        }

测试这个方法的逻辑,只需要下面这段代码就可以了:

        private bankaccount bankaccounta;
        private bankaccount bankaccountb;
        [setup]
        public void setup()
        {
            bankaccounta = new bankaccount(1000);
            bankaccountb = new bankaccount(1000);
        }

        [test]
        public void transfer_test()
        {
            ibankservice bankservice = new bankservice();
            bankservice.transferaccounts(bankaccounta, bankaccountb, 500);
            assert.areequal(500, bankaccounta.getbalance());
            assert.areequal(1500, bankaccountb.getbalance());
        }

但,如果转账的逻辑变了,需要判断是否超过当日限制,那么用户的转账总额就得从数据库或者其它途径获得了,那么可能代码变成这样子:

        private readonly ibanklimitdao _banklimitdao;//获取限制条件的类

        public bankservice(ibanklimitdao banklimitdao)
        {
            _banklimitdao = banklimitdao;
        }

        /// <summary>
        /// 转账
        /// </summary>
        /// <param name="accounta"></param>
        /// <param name="accountb"></param>
        /// <param name="money"></param>
        /// <returns></returns>
        public double transferaccounts(bankaccount accounta, bankaccount accountb, double money)
        {
            double transferlimit = 50000.0;//转账最高限制
            try
            {
                //判断a是否能转账
                var total = _banklimitdao.totaltransfertotal(accounta.accountid);//获得限制金额
                if (total >= transferlimit)
                {
                    throw new exception($"超过当日转账限额{transferlimit}");
                }
                var balancea = accounta.drawmoney(money);
                accountb.savemoney(money);
                return balancea;
            }
            catch (exception ex)
            {
                throw new exception($"转账失败,{ex.message}");
            }
        }

这个时候再用真实对象来测试就有点麻烦了。根据隔离原则,我们不希望测试 totaltransfertotal 方法里的逻辑和它的正确性,它应该在其它地方测试。这时候mock就显得重要了,我们可以模拟这个对象,并且给它一个恰当的值,让它“正确”执行。

所以,测试代码变成这样子:

        [test]
        public void transfer_test()
        {
            var banklimit = mock.create<ibanklimitdao>();//模拟对象
            mock.arrange(() => banklimit.todaldrawtotal(arg.isany<string>())).returns(500);//设定一个返回值
            ibankservice bankservice = new bankservice(banklimit);
            bankservice.transferaccounts(bankaccounta, bankaccountb, 500);
            mock.assert(banklimit);
            assert.areequal(500, bankaccounta.getbalance());
            assert.areequal(1500, bankaccountb.getbalance());
        }

aaa

什么是aaa?arrange、act和assert。aaa是单元测试中编写代码的模式。

  • arrange:准备,设置需要测试的对象。
  • act:执行测试的实际代码。
  • assert:验证结果。

一个简单的例子:
这个例子包括创建模拟对象,标记为inorder(),意为必须调用,执行方法,最后用mock.assert验证。

public interface ifoo 
{ 
    void submit(); 
    void echo(); 
} 
[test]
public void shouldverifycallsorder()
{
    // arrange 模拟对象,并且设置条件
    var foo = mock.create<ifoo>();

    mock.arrange(() => foo.submit()).inorder();
    mock.arrange(() => foo.echo()).inorder();

    // act 执行代码
    foo.submit();
    foo.echo();

    // assert 验证结果
    mock.assert(foo);
} 

编写测试方法的时候尽量遵循aaa的模式编写,可以让测试代码更清晰可读。

mock behaviors

justmock 在mock对象的时候有四种不同的行为可以选择。

  • recursiveloose behavior
    默认的选项。模拟的对象不会出现null对象,递归调用也将创建一个默认的对象、默认值或者空值。
  • loose behavior
    除了设置值,否则loose创建的对象将是默认值。
  • calloriginal behavior
    将会采用最初的模拟对象。
  • strict behavior
    采用此行为,模拟对象必须设置值,否则会出现 mockexception异常。

下面代码展示不同类型的结果:

        [test]
        public void test()
        {
            // arrange 
            var rlfoo = mock.create<foobase>(behavior.recursiveloose);
            var lfoo = mock.create<foobase>(behavior.loose);
            var cofoo = mock.create<foobase>(behavior.calloriginal);
            var sfoo = mock.create<foobase>(behavior.strict);

            mock.arrange(() => rlfoo.getstring("y")).returns("z");
            mock.arrange(() => lfoo.getstring("y")).returns("z");
            mock.arrange(() => cofoo.getstring("y")).returns("z");
            mock.arrange(() => sfoo.getstring("y")).returns("z");

            // act 
            var rlactualx = rlfoo.getstring("x"); // 结果:""
            var rlactualy = rlfoo.getstring("y"); // 结果:"z"

            var lactualx = lfoo.getstring("x"); // 结果:null
            var lactualy = lfoo.getstring("y"); // 结果:"z"

            var coactualx = cofoo.getstring("x"); // 结果:"x"
            var coactualy = cofoo.getstring("y"); // 结果:"z"
            var coactuala = cofoo.getstring("a"); // 结果:"a"

            //var sactualx = sfoo.getstring("x"); // 结果:出现异常
            var sactualy = sfoo.getstring("y"); // 结果:"z"  

            var expectedx = "x";
            var expectedy = "z";

            // assert 
            assert.areequal(expectedx, rlactualx);
            assert.areequal(expectedy, rlactualy);
        }

本篇到这,下篇再记录一些其它用法。