Hello World
Spiga

与protected成员有关的单元测试方式

2009-08-28 17:33 by 老赵, 4958 visits

这是一篇简单的文章,讨论了单元测试中遇到protected成员的应对方案。此外,在文章最后也希望和大家讨论一下某个特殊的情况下的处理方法。

protected是一个有趣而有用的修饰符,它把方法的访问成员严格限制在自身或自己的子类身上。换句话说,在使用过程中,protected成员对外部是开放的(因为其他类可以通过继承来使用该成员),又是封闭的(不是自身或子类的一切成员都无法访问)。而对于单元测试来说,protected成员又是尴尬的,因为它的“开放”意味着我们必须对它进行单元测试,而“封闭”又阻碍了我们在单元测试中涉及protected成员。

测试protected方法

现在有一个类,其中包含一个protected方法:

public class SomeClass
{
    protected int SomeMethod(string arg) { ... }
}

如果我们需要对这个protected方法进行单元测试,可以在测试代码中准备一个辅助类型:

public class SomeClassForTest : SomeClass
{
    public int PublicSomeMethod(string arg)
    {
        return this.SomeMethod(arg);
    }
}

于是在单元测试中,便可以通过调用PublicSomeMethod来测试基类的SomeMethod方法:

var testClass = new SomeClassForTest();
var result = testClass.PublicSomeMethod(null);
Assert.Equal(0, result);

非常简单。

如果您觉得麻烦,也可以将SomeClass类中的SomeMethod方法改为protected internal,这样便可以在InternalVisibleTo的测试程序集中使用了。不过,我觉得为单元测试而改变成员的访问级别不是一个合适的做法。

对protected方法进行Mock

现在有一个类,其中有一个protected方法:

public class SomeClass
{
    protected virtual int SomeMethod(string arg) { ... }
}

并且,某个被测试的方法接受SomeClass作为参数。虽然被测试的方法不会直接调用SomeMethod方法,但是SomeMethod的实现会影响到公开接口的表现形式。于是,我们需要对SomeMethod进行Mock或Stub。为此,我们同样需要准备一个辅助类型:

public class MockSomeClass : SomeClass
{
    protected override int SomeMethod(string arg)
    {
        return this.PublicSomeMethod(arg);
    }

    public virtual int PublicSomeMethod(string arg)
    {
        return base.SomeMethod(arg);
    }
}

在MockSomeClass中,我们覆盖了基类的SomeMethod实现,使它调用了子类中公开的PublicSomeMethod方法,而PublicSomeMethod内部又调用了基类的SomeMethod方法。因此,如果您不去进行任何处理,那么MockSomeClass会保持SomeMethod的实现不变。而如果您需要对SomeMethod进行Mock或Stub的时候,便可以从PublicSomeMethod下手:

Mock<MockSomeClass> mockSomeClass = new Mock<MockSomeClass>() { CallBase = true };
mockSomeClass.Setup(c => c.PublicSomeMethod("123")).Returns(123);

DoSomeTest(mockSomeClass.Object); // use the mock object

也很容易。

为了可测试性

值得注意的是,为了“可测试性”,第二部分中的protected方法必须是virtual的,因为我们需要在子类中进行override。同理,Mock框架能够辅助的方法也必须是virtual的,即使是一个public方法。那么,您觉得这是为了可测试性而做出的让步吗?或者换句话说,您觉得,一个不可以override的protected方法,但是会影响到其他公开接口的功能,这是不是一个合理的设计呢?如果这是一个合理的设计,又不想作出这样的让步……我们又该怎么做呢?

关于这点,我有自己的想法,不过还是想先听一下您的意见。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

40 条回复

  1. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-08-28 17:40:00

    big chair
    Mock的谓词真的很可怕。
    老赵最近在整什么啊?测试?

  2. Jooly
    *.*.*.*
    链接

    Jooly 2009-08-28 17:40:00

    高产。。(我是沙发)

  3. craboYang
    *.*.*.*
    链接

    craboYang 2009-08-28 18:06:00

    您的测试需要覆盖这么细?
    我通常只在Facade层确认I/O就罢了。
    我只用NUnit,连Mock都没到用过,因为居然没需要-_-!

  4. singleView
    *.*.*.*
    链接

    singleView 2009-08-28 18:15:00

    赵老大,最近活跃啊,哪个IE6.0的提示是不是考虑给撤消了

  5. egmkang
    *.*.*.*
    链接

    egmkang 2009-08-28 18:34:00

    @singleView
    除了网银,其他都不用IE,就别说6了

  6. 老赵
    admin
    链接

    老赵 2009-08-28 18:48:00

    @craboYang
    测试覆盖率就70-80%总归会有的啊。
    不过现在只是谈了测试组件会遇到的情况,总归有用的,呵呵。

  7. 老赵
    admin
    链接

    老赵 2009-08-28 18:49:00

    DiggingDeeply:
    老赵最近在整什么啊?测试?


    老老实实为一个ASP.NET MVC项目打基础。

  8. 老赵
    admin
    链接

    老赵 2009-08-28 19:02:00

    @singleView
    不,严格抵制IE6。

  9. xiaotie
    *.*.*.*
    链接

    xiaotie 2009-08-28 19:46:00

    俺一般是提供一个测试桩:

    protected void SomeMethod()

    #if debug
    public void SomeMethodStub()
    {
    SomeMethod();
    }
    #endif

    testcase:

    #if debug
    public void TestSomeMethod()
    {
    ......
    }
    #endif

  10. 老赵
    admin
    链接

    老赵 2009-08-28 19:57:00

    @xiaotie
    没看懂啊,这是什么意思?

  11. xiaotie
    *.*.*.*
    链接

    xiaotie 2009-08-28 20:15:00

    @Jeffrey Zhao
    把要测试的方法简单的放到另一个public方法之中暴露出来,然后外面围上
    #if DEBUG
    #endif
    确保不会再release中被暴露。
    然后把测试项目中的测试方法也用 #if DEBUG #endif 围起来。
    俺经常这样测试private方法。

  12. Arison[未注册用户]
    *.*.*.*
    链接

    Arison[未注册用户] 2009-08-28 20:35:00

    不是看不起你,除了写鸡毛蒜皮的东西,小儿科的理论,

    还能整除什么名堂,也就一个实习生的水平线,

    学了这么多东西到处去忽悠这么好那么好,

    根本不知道企业开发要用到什么,实际需求是什么,

    一纯拿技术取乐的人,一天到晚在这里反复着,为什么

  13. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-08-28 21:18:00

    譬如说partial class

  14. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-08-28 21:20:00

    @Arison
    难道平时可以打球,就不能折腾点代码?

  15. 老赵
    admin
    链接

    老赵 2009-08-28 21:59:00

    @vczh
    什么partial class?

  16. 老赵
    admin
    链接

    老赵 2009-08-28 21:59:00

    @Arison
    哎,其实我也挺可怜您的,您说您一个看到技术就苦大仇深的人,怎么能体会到我这个拿技术取乐的人的幸福呢?
    有一点您说的没错,我是不知道企业开发要用什么,因为我是做互联网的。信不信由您,不少公司都挺看得上我的呢。

  17. 王德水
    *.*.*.*
    链接

    王德水 2009-08-28 22:20:00

    我们现在只要求开发人员能保证自己写的代码还有是要有单元测试,要求代码没有bug,修改时用测试做保障,至于需不需要公开,那是第二步的,如果有好的架构师设计出可测试行高的代码,,但没有的时侯,public是解决可测试行更快的办法

  18. 老赵
    admin
    链接

    老赵 2009-08-28 22:43:00

    @王德水
    嗯,用public是最快最方便的。
    不过其实,有时候只是在某些情况下“不知道怎么设计成可测试性”。
    我这些东西算是一种总结,也就是说,提供一种特定情况下的重构方式。
    Kent Beck帮这些东西取了个好名字,叫做“实现模式”,赫赫。

  19. Arison[未注册用户]
    *.*.*.*
    链接

    Arison[未注册用户] 2009-08-28 22:48:00

    Jeffrey Zhao:
    @Arison
    哎,其实我也挺可怜您的,您说您一个看到技术就苦大仇深的人,怎么能体会到我这个拿技术取乐的人的幸福呢?
    有一点您说的没错,我是不知道企业开发要用什么,因为我是做互联网的。信不信由您,不少公司都挺看得上我的呢。



    为什么你会知道我怎么看待技术,又何来苦大仇深,其实你根本就是猜的,无中

    生有,无疑你确实异想天开的那头了

    为什么你就一定知道一个企业开发就是和网络无关,这也是你凭空捏赵,又有谁

    会去相信一个一厢情愿,本来就没有什么干戏


  20. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-28 22:56:00

    Jeffrey Zhao:
    @vczh
    什么partial class?


    就是把一个类型写在两个文件里面。一个是产品用的,该私有的就私有。另外一个,包了一组供测试用的Wrapper方法。

  21. 老赵
    admin
    链接

    老赵 2009-08-28 22:59:00

    @Arison
    我说的是“互联网应用”,不是“和网络有关的应用”,赫赫,我们还是明确一下定义比较好。
    至于我对您的猜测,嗯嗯,的确是我错了,希望您不要介意。
    不过我还是推测,您一定是个成熟的科学家,所以对于我搞这些工程上的,或者趣味性的东西不屑一顾,委屈您了。
    不过,您能否给我一些指点,让我知道我该努力的方向呢?

  22. 老赵
    admin
    链接

    老赵 2009-08-28 23:00:00

    @Colin Han
    partial class的概念和作用我清楚,但是partial class只是编辑上的概念,最终编译之后就和写在一个文件里没有区别了,这和单元测试有什么关系呢?

  23. Arison[未注册用户]
    *.*.*.*
    链接

    Arison[未注册用户] 2009-08-28 23:13:00

    Jeffrey Zhao:
    @Arison
    我说的是“互联网应用”,不是“和网络有关的应用”,赫赫,我们还是明确一下定义比较好。
    至于我对您的猜测,嗯嗯,的确是我错了,希望您不要介意。
    不过我还是推测,您一定是个成熟的科学家,所以对于我搞这些工程上的,或者趣味性的东西不屑一顾,委屈您了。
    不过,您能否给我一些指点,让我知道我该努力的方向呢?



    “互联网应用” 不是 “和网络有关的应用”,这就是你要说的吧

    为什么会介意,就为几句话不是很不值得,况且又是出自这么样的一个人

    不要把你的意愿强加给别人,没有人有你要得那义务



  24. 老赵
    admin
    链接

    老赵 2009-08-28 23:17:00

    @Arison
    我是说,我搞得是“互联网应用”,不是说我在搞“和网络有关的应用”。
    好吧好吧,咱们还是别讨论这个定义了。您能否给我指明一下,我现在做错了什么,又该朝什么方向努力呢?

  25. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-28 23:38:00

    @Jeffrey Zhao
    可以建两个项目,一个给单元测试用,其中包含那个给测试用的Partial文件。另外一个是产品用的。不包含那个文件。

  26. 老赵
    admin
    链接

    老赵 2009-08-28 23:42:00

    @Colin Han
    这个想法挺奇特的,不过需要在两个项目之间作同步,这点需要自己准备额外的工具或程序或机制什么的吧。
    你有没有这样做过?

  27. Nick Wang (懒人王)
    *.*.*.*
    链接

    Nick Wang (懒人王) 2009-08-28 23:47:00

    @Arison
    首页帖子放眼望去,可以说有90%都是和我想法不一致的,可是不是不一致的就一定要批呢?

    都是有脑子的成年人,没人要你一定如何如何,你也没必要让别人如何。

  28. dali[未注册用户]
    *.*.*.*
    链接

    dali[未注册用户] 2009-08-29 08:16:00

    private 怎么测试

  29. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-08-29 09:14:00

    @Arison
    不要自取其辱
    “王二用自己的脑袋和石头做了一个些微的较量”
    “王二输了”

  30. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-08-29 09:29:00

    我觉得 为了测试把protected 增加virtual 也有坏味道
    理由也很简单 确定的逻辑对修改关闭

    public class MockSomeClass : SomeClass
    {
    
    
        public int SomeMethod(string arg, bool meaningLessParam)
        {
            return base.SomeMethod(arg);
        }
    }
    
    


    这样测试行不行呢 反正多个无用参数而已

  31. 老赵
    admin
    链接

    老赵 2009-08-29 13:30:00

    @韦恩卑鄙
    真要这样用,总归还是可以的,不过我不太喜欢这个方式。

  32. 老赵
    admin
    链接

    老赵 2009-08-29 13:31:00

    @dali
    下次谈,我倾向于不测试,把要测试的逻辑提取出去,因为要测试,往往意味着较为复杂。

  33. Nosira[未注册用户]
    *.*.*.*
    链接

    Nosira[未注册用户] 2009-08-29 20:19:00

    @Arison
    最看不惯的就是你这种人,装得自己多高深似的

  34. xiaowen[未注册用户]
    *.*.*.*
    链接

    xiaowen[未注册用户] 2009-08-30 16:09:00

    Nosira:
    @Arison
    最看不惯的就是你这种人,装得自己多高深似的



    就像在吃饭的时候听到嗡嗡嗡的声音...

  35. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-08-30 22:19:00

    @Jeffrey Zhao
    Colin Han同学充分了解了我的意思啊。你只要把一个class标记成partial,在test project里面写另一个partial与之配对就可以了。你一个Class一般都会有一个Test Class的:

    project:
    class A;
    test project:
    class ATest;

    ---->

    project:
    partial class A;
    test project:
    partial class A;这里暴露一些非公开的东西
    class ATest;

    所以不怎么需要维护。而且你接口变了,原本需要的修改只会变多一点点。

  36. 老赵
    admin
    链接

    老赵 2009-08-30 22:26:00

    @vczh
    partial class怎么可以跨project的?
    Colin Han说的只是在test时多编译一些partial class进去吧。

  37. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-08-31 09:41:00

    @Jeffrey Zhao
    写prebuild event将代码复制过来就行了

  38. 老赵
    admin
    链接

    老赵 2009-08-31 09:45:00

    @vczh
    这和我之前想象地就比较类似了……不过实际操作起来,你能再仔细谈谈吗?

  39. 似水之心
    *.*.*.*
    链接

    似水之心 2009-08-31 15:54:00

    细到这种程度了啊

    仰慕一下

  40. a17366
    110.90.243.*
    链接

    a17366 2014-07-23 02:21:41

    个人认为, 首先需要确定测试目的,状态验证还是行为验证。 其次,既然protected(private)方法复杂到需要测试,不如重构代码(按测试目的重构)。 将方法提取至新类,转而测试新类。不同目的,重构代码不同,需就事论事,不展开谈了。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我