TDD个人实践体会(C#)三

上一篇,完成了最初设计的测试代码的编写和能让测试代码运行的功能代码的编写。

虽说还没有真正实现核心的,关于求排列和求组合的算法的代码,但是在编写代码的过程中,已经感觉到最初的设计,可能存在某些缺陷,因为可以看到测试代码中存在很多的bad smell的代码。

既然发现了这些代码,就要想办法来重构了。

先来看一下目前的设计和测试代码

 

设计  

1、我需要创建一个类库,暂且命名为:MathLibrary
2、类库内包含 ComposerSelector<T> 和 PermutationSelector<T> 两个类
3、ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类
4、Selector<T> 包含了 T[] SourceObjects 属性和 int CountToSelect 属性,来存储数据源和要从中取出的排列组合要包含的项目个数
5、ComposerSelector<T> 和 PermutationSelector<T> 对象,可以调用DoProcess方法来进行排列组合的运算
6、Selector<T> 包含了 Result 属性,这个属性用来存储 DoProcess 方法运算后的结果值
7、Selector<T> 包含 ResultCount 属性,返回结果包含的记录条数

 

ComposerTest 类内的代码 

        [TestMethod]
        public void TestCreateComposerSelector()
        {
            ComposerSelector<GenericParameterHelper> generalSelector = TestCreateComposerSelector<GenericParameterHelper>(new GenericParameterHelper[] { }, 5);
            ComposerSelector<int> intSelector = TestCreateComposerSelector<int>(new int[] { }, 10);
            ComposerSelector<string> stringSelector = TestCreateComposerSelector<string>(new string[] { }, 3);
            ComposerSelector<object> objectSelector = TestCreateComposerSelector<object>(new object[] { }, 9);
        }

        private ComposerSelector<T> TestCreateComposerSelector<T>(T[] source, int countToSelect)
        {
            return new ComposerSelector<T>(source, countToSelect);
        }
        
        [TestMethod]
        public void ComposerPropertiesTest() {
            int[] intSource = new int[] { 0123456789 };
            int intCountToSelect = 4;
            ComposerSelector<int> intSelector = new ComposerSelector<int>(intSource, intCountToSelect);
            intSelector.DoProcess();
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
            Assert.IsNotNull(intSelector.Result);



            object[] objSource = new object[] { 10'A'"HelloWorld"2.69ftrue };
            int objCountToSelect = 3;
            ComposerSelector<object> objSelector = new ComposerSelector<object>(objSource, objCountToSelect);
            objSelector.DoProcess();
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
            Assert.IsNotNull(objSelector.Result);

        }

 

PermutationTest 类内的代码 

        [TestMethod]
        public void TestCreatePermutationSelector()
        {
            PremutationSelector<GenericParameterHelper> generalSelector = TestCreatePermutationSelector<GenericParameterHelper>(new GenericParameterHelper[] { }, 5);
            PremutationSelector<int> intSelector = TestCreatePermutationSelector<int>(new int[] { }, 10);
            PremutationSelector<string> stringSelector = TestCreatePermutationSelector<string>(new string[] { }, 3);
            PremutationSelector<object> objectSelector = TestCreatePermutationSelector<object>(new object[] { }, 9);
        }

        private PremutationSelector<T> TestCreatePermutationSelector<T>(T[] source, int countToSelect)
        {
            return new PremutationSelector<T>(source, countToSelect);
        }

        [TestMethod]
        public void PremutationPropertiesTest()
        {
            int[] intSource = new int[] { 0123456789 };
            int intCountToSelect = 4;
            PremutationSelector<int> intSelector = new PremutationSelector<int>(intSource, intCountToSelect);
            intSelector.DoProcess();
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
            Assert.IsNotNull(intSelector.Result);


            object[] objSource = new object[] { 10'A'"HelloWorld"2.69ftrue };
            int objCountToSelect = 3;
            PremutationSelector<object> objSelector = new PremutationSelector<object>(objSource, objCountToSelect);
            objSelector.DoProcess();
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
            Assert.IsNotNull(objSelector.Result);

        }

 

首先可以看到,最初为了测试构造函数而编写的selector实力对象的创建的测试代码(已经用蓝灰色标注),因为在后面的测试代码中已经包含了这些行为,因此,可以预期,如果删除这部分代码,并不会影响我们的测试效果。(为了谨慎起见,我个人是先将这些代码注释后运行了测试用例,检查测试代码覆盖率仍旧是100%后,删除掉了这些代码。)

 

删除重复代码

编译:通过

测试:通过

 

然后,在两个测试文件内的测试代码,仍旧是一模一样的,只是两个类不一样而已,属性、方法,方法要做的事情,几乎都是完全相同,只有DoProcess的算法稍有不同。

是否可以将其保留为只有一个类,将DoProcess行为的不同,放在类内来解决,我稍微在便签上画了一下,了解到要解决的是DoProcess的多态。为了尽快的验证是否可行,用了最简单的方法参数多态来解决(其实后面会用其他方法解决,只是当时我最先用这个方法来解决的,便一步步写出来,OK,重构设计

在重构设计的时候,我发现我原本对设计进行编号的方法极其错误,这让我无法确定修改的设计应该放在设计的那个位置,我于是删除了原本设计中的编号,用红色表示已经实现的设计,用灰色表示删除的设计,用蓝色表示新增的设计

 

设计 

我需要创建一个类库,暂且命名为:MathLibrary
类库内包含 ComposerSelector<T> 和 PermutationSelector<T> 两个类
类库包含Selector<T>类
ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类
Selector<T> 包含了 T[] SourceObjects 属性和 int CountToSelect 属性,来存储数据源和要从中取出的排列组合要包含的项目个数
ComposerSelector<T> 和 PermutationSelector<T> 对象,可以调用DoProcess方法来进行排列组合的运算
Selector<T> 的对象,可以运行 DoProcess 方法来进行排列组合的运算,DoProcess 根据传入枚举参数 SelectType 来进行不同的逻辑运算
Selector<T> 包含了 Result 属性,这个属性用来存储 DoProcess 方法运算后的结果值
Selector<T> 包含 ResultCount 属性,返回结果包含的记录条数

 

新增了一个测试类 SelectorTest , 新增了 DoSelectorTest 的测试方法

将ComposerTest类中的测试方法复制到了SelectorTest之后,我删除了两个ComposerTest和PermutationTest

然后将SelectorTest中创建ComposerSelector对象全部改为了创建Selector对象,Selector对象的DoProcess方法,加入了一个枚举参数 SelectType ,新测试代码如下

 

        [TestMethod]
        public void DoSelectorTest() {
            int[] intSource = new int[] { 0123456789 };
            int intCountToSelect = 4;
            Selector<int> intSelector = new Selector<int>(intSource, intCountToSelect);
            intSelector.DoProcess(SelectType.Compose);
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
            Assert.IsNotNull(intSelector.Result);

            object[] objSource = new object[] { 10'A'"HelloWorld"2.69ftrue };
            int objCountToSelect = 3;
            Selector<object> objSelector = new Selector<object>(objSource, objCountToSelect);
            objSelector.DoProcess(SelectType.Permutation);
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
            Assert.IsNotNull(objSelector.Result);
        }

 

编译:失败 ;

错误 3 无法创建抽象类或接口“MathLibrary.Selector<object>”的实例

错误 1 无法创建抽象类或接口“MathLibrary.Selector<int>”的实例
错误 2 当前上下文中不存在名称“SelectType”
错误 4 当前上下文中不存在名称“SelectType”

原本我们并不希望实例化Selector,现在我们修改了我们的设计后,将 Selector 的 abstract 修饰去掉;

新增一个SelectType的枚举在MathLibrary库,包括了 Compose 和 Premutation 两个值

(这里我就不写修改的代码了)

 

编译:失败

错误 2 “MathLibrary.Selector<T>.DoProcess()”是抽象的,但它包含在非抽象类“MathLibrary.Selector<T>”中 

 

去掉DoProcess()的abstract修饰并且加上声明主体;

删除ComposerSelector和PremutationSelector两个类

编译:失败

哦,我忘记了给DoProcess加上SelectType的参数了,加好参数

 

编译:成功

测试:失败

未通过 DoSelectorTest MathLibraryTest Assert.IsNotNull 失败。 

原来是并未在DoProcess中加入对Result的赋值,因此对Result的IsNotNull判断失败。

这里我传入了SelectType的参数来实现DoProcess的多态,便需要在测试代码中对这个多态进行测试

假设如果传入 SelectType.Compose , Result 为 Null;

如果传入 SelectType.Permutation, 构建一个包含了一条记录的Result,记录值为 SourceObjects;

我们再次修改测试代码

 

        [TestMethod]
        public void DoSelectorTest()
        {
            int[] intSource = new int[] { 0123456789 };
            int intCountToSelect = 4;
            Selector<int> intSelector = new Selector<int>(intSource, intCountToSelect);
            intSelector.DoProcess(SelectType.Compose);
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
            Assert.IsNull(intSelector.Result);

            object[] objSource = new object[] { 10'A'"HelloWorld"2.69ftrue };
            int objCountToSelect = 3;
            Selector<object> objSelector = new Selector<object>(objSource, objCountToSelect);
            objSelector.DoProcess(SelectType.Permutation);
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
            Assert.IsNotNull(objSelector.Result);

        }

 加粗斜体的蓝色代码使我们修改的那行。

然后我们在DoProcess加入

 

        public void DoProcess(SelectType selecType) {
            if (selecType == SelectType.Compose)
                this.Result = null;
            if (selecType == SelectType.Permutation)
                this.Result = new List<T[]>() { SourceObjects };
        }

 

编译:通过

测试:通过

代码覆盖率 100 %

 

 

目前的设计

 

我需要创建一个类库,暂且命名为:MathLibrary
类库内包含 ComposerSelector<T> 和 PermutationSelector<T> 两个类
类库包含Selector<T>类
ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类
Selector<T> 包含了 T[] SourceObjects 属性和 int CountToSelect 属性,来存储数据源和要从中取出的排列组合要包含的项目个数
ComposerSelector<T> 和 PermutationSelector<T> 对象,可以调用DoProcess方法来进行排列组合的运算
Selector<T> 的对象,可以运行 DoProcess 方法来进行排列组合的运算,DoProcess 根据传入枚举参数 SelectType 来进行不同的逻辑运算
Selector<T> 包含了 Result 属性,这个属性用来存储 DoProcess 方法运算后的结果值
Selector<T> 包含 ResultCount 属性,返回结果包含的记录条数

你一定已经 注意到了,一直有个ResultCount被遗留在这里,我不想说我为了什么什么原因故意这样做的,其实,我根本是忘了这码事。

不过,还好,我在检查设计时候,发现了这一条设计,而我的测试代码里并没有这个ResultCount的测试,于是,我们补充进去。

按照我们目前为实现的DoProcess,ResultCount在 DoProcess(SelectType.Compose)后应该为0;而在DoProcess(SelectType.Permutation)后应该为 1

我们补充我们的测试代码

 

 

            int[] intSource = new int[] { 0123456789 };
            int intCountToSelect = 4;
            Selector<int> intSelector = new Selector<int>(intSource, intCountToSelect);
            intSelector.DoProcess(SelectType.Compose);
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
            Assert.IsNull(intSelector.Result);
            Assert.AreEqual(intSelector.ResultCount, 0);

            object[] objSource = new object[] { 10'A'"HelloWorld"2.69ftrue };
            int objCountToSelect = 3;
            Selector<object> objSelector = new Selector<object>(objSource, objCountToSelect);
            objSelector.DoProcess(SelectType.Permutation);
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
            Assert.IsNotNull(objSelector.Result);
            Assert.AreEqual(objSelector.ResultCount, 1);

 

然后在Selector增加ResultCount的属性和get的实现

 

public int ResultCount { get { return this.SourceObjects.Count(); } }

 

编译:通过

测试:失败 未通过 DoSelectorTest MathLibraryTest Assert.AreEqual 失败。应为: <10>,实际为: <0>。  

这里出现了一条完全让我莫名其妙的失败,按照逻辑,要么是0,要么是1,怎么会有一个10的值出现呢?

检查了一下代码,原来Result.的get方法,应该是 return this.Result.Count;我写成了return this.SourceObjects.Count(); (TDD的威力,如果我没有TDD,这个错误,我有可能会在很久以后才发现),修正代码

编译:通过

测试:失败 测试方法 MathLibraryTest.SelectorTest.DoSelectorTest 引发了异常:  System.NullReferenceException: 未将对象引用设置到对象的实例。

我们希望在Result是null的时候,返回0,而这里调用.Cout显然会在null值时候报上面的错误

 

再次修改: 

 

 

public int ResultCount { get { return this.Result == null ? 0 : this.Result.Count; } }

 

 编译:通过

测试:通过

现在的设计。

我需要创建一个类库,暂且命名为:MathLibrary
类库内包含 ComposerSelector<T> 和 PermutationSelector<T> 两个类
类库包含Selector<T>类
ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类
Selector<T> 包含了 T[] SourceObjects 属性和 int CountToSelect 属性,来存储数据源和要从中取出的排列组合要包含的项目个数
ComposerSelector<T> 和 PermutationSelector<T> 对象,可以调用DoProcess方法来进行排列组合的运算
Selector<T> 的对象,可以运行 DoProcess 方法来进行排列组合的运算,DoProcess 根据传入枚举参数 SelectType 来进行不同的逻辑运算
Selector<T> 包含了 Result 属性,这个属性用来存储 DoProcess 方法运算后的结果值
Selector<T> 包含 ResultCount 属性,返回结果包含的记录条数

 

先到这里,后面我们实现算法的过程中,会不断的编码、测试、重构、测试;编码、测试、重构、测试,如此循环。

 

====================================

直到现在,我们还没有写算法的实现,但是Test代码的编写,确实让我们对设计进行了不断的优化。

我知道我有点啰嗦,你也许会说,兄弟,这会儿时间,说不定,我早就已经写完功能代码了(事实也的确可能是这样的)。

这里我不得不说一下我的感觉

一,这里我虽说啰啰嗦嗦说了一大堆,但是在实际编码时候,可能只是二十分钟时间

二,在这次TDD的实践过程中,我一直保持着对自己代码的信心,我知道,只要通过测试,我的代码就是按照我的想法在运行,那种以前写代码时候惴惴不安,不知道自己的代码什么时候会出莫名其妙的问题的感觉少了很多(这种感觉让我超爽)

 

===============另外一些题外话(我的个人愚见,欢迎讨论)====================

有的朋友也许说,为什么非要先写测试代码呢?多费劲,先创建对象,定义属性和方法,然后右键-生成单元测试,这个功能多爽

这个功能的确很方便创建测试代码,但是我并不喜欢的原因

一、生成的测试代码也许并不是你想要的行为(例如,我想对某个{public get;private set;}的属性的读写进行测试,系统会自动生成一个Accessor类,让你来能调用private set的方法,而我则是想通过public 出来的方法来测试set; 这里由于 private set 会被生成的测试代码在外部覆盖,有可能造成我的所有方法,都没有对 private set 赋值,但是这个 set 方法却在代码覆盖率的统计中被统计为已被覆盖;而我的真实实现中可能根本无法覆盖set方法)

二、关于对类内private属性和方法的测试;有人可能说,你如果不用Accessor方法,就无法方便的测到private 的方法和属性;关于这一点,我想说,如果一个private的方法或者属性,我们用尽了各种Test case,都无法在外部实现它的覆盖,那么我们就要考虑,这儿方法或者属性是否真的有存在的必要了。

posted on 2012-06-07 23:37  KangPeng  阅读(2468)  评论(2编辑  收藏  举报

导航