《重构》C#版实现(一)构建参考结果的测试用例

首先,列出的是第一个“电影租赁”案例的C#版初始代码:

using System;
using System.Collections.Generic;

namespace CH01_MovieRentalHouse
{
    public class Movie
    {
        public const int CHILDRENS = 2;
        public const int REGULAR = 0;
        public const int NEW_RELEASE = 1;

        public string Title { get; private set; }
        public int PriceCode { get; private set; }
        
        public Movie(string title, int priceCode)
        {
            Title = title;
            PriceCode = priceCode;
        }
    }

    public class Rental
    {
        public Movie Movie { get; private set; }
        public int DaysRented { get; private set; }

        public Rental(Movie rented, int days)
        {
            Movie = rented;
            DaysRented = days;
        }
    }

    public class Customer
    {
        public string Name { get; private set; }

        private List<Rental> Rentals = new List<Rental>();

        public Customer(string name)
        {
            Name = name;
        }

        public void Add(Rental rental)
        {
            Rentals.Add(rental);
        }

        public string Statement()
        {
            double totalAmount = 0;
            int frequentRenterPoints = 0;
            string result = "Rental Record for " + Name + "\n";
            foreach (Rental rental in Rentals)
            {
                double thisAmount = 0;
                
                // determine amounts for each line
                switch (rental.Movie.PriceCode)
                {
                    case Movie.REGULAR:
                        thisAmount += 2;
                        if (rental.DaysRented > 2)
                            thisAmount += (rental.DaysRented - 2) * 1.5;
                        break;
                    case Movie.NEW_RELEASE:
                        thisAmount += rental.DaysRented * 3;
                        break;
                    case Movie.CHILDRENS:
                        thisAmount += 1.5;
                        if (rental.DaysRented > 3)
                            thisAmount += (rental.DaysRented - 3) * 1.5;
                        break;
                }

                // add frequent renter points
                frequentRenterPoints++;
                // add bonus for a two day new release rental
                if (rental.Movie.PriceCode == Movie.NEW_RELEASE &&
                    rental.DaysRented > 1) frequentRenterPoints++;
                
                // show figures for this rental
                result += "\t" + rental.Movie.Title + "\t" + thisAmount.ToString() + "\n";
                totalAmount += thisAmount;
            }
            // add footer lines
            result += "Amount owed is " + totalAmount.ToString() + "\n";
            result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
            return result;
        }
    }
}

  在工作中,经常见到动辄数百,甚至上千行的方法。对于那些程序员们,我实在不知道他们拥有怎样的技巧,才能工作在复杂如斯的代码谜团中,同时还能(真的能么?)保证代码不出错。以个人经验及数据来说,结构良好的代码中,类方法的长度约有40%为1-5行,50%为5到15行左右,还有10%为15-50行。虽然不能严格地说方法的代码行数越少越好,但简短且职责更单一的方法确实成为了我衡量代码的重要标准之一。

  《重构》一书中提到了一个重要概念:“每当我要进行重构的时候,第一个步骤永远相同:我得为即将修改的代码建立一组可靠的测试环境”。诚然,没有测试也能进行重构,但没有它的保障,大多数重构行为都会以失败告终。所以,后面所有的重构,都会在C#的单元测试框架基础上完成,而这也是《重构》中没有给出的部分(相当重要的部分)。
  首先,必须确定的是初始代码的正确性,这是重构的基础之一,如果原始代码不正确,我们也就无法用它来得到可供参考的结果。那么,第一步工作不是立刻大刀阔斧地对现有代码进行重构,而是先使用正确的初始代码运行几分测试数据,得到相应的结果。为此,添加如下代码:

    class Program
    {
        static void Main(string[] args)
        {
            Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
            Movie godFather = new Movie("GodFather", Movie.REGULAR);
            Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);

            Rental bh4DayRental = new Rental(braveHeart, 4);
            Rental bh2DRental = new Rental(braveHeart, 2);
            Rental we7DRental = new Rental(wallE, 7);
            Rental gfRental = new Rental(godFather, 5);
            Rental we3DRental = new Rental(wallE, 3);

            Customer Charles = new Customer("Charles");
            Charles.Add(bh4DayRental);
            Charles.Add(gfRental);

            Customer Jess = new Customer("Jess");
            Jess.Add(bh4DayRental);
            Jess.Add(we7DRental);
            Jess.Add(bh2DRental);

            Customer Rebecca = new Customer("Rebecca");
            Rebecca.Add(we7DRental);
            Rebecca.Add(we3DRental);

            Customer Lucas = new Customer("Lucas");
            Lucas.Add(bh2DRental);
            Lucas.Add(we7DRental);
            Lucas.Add(we3DRental);

            Console.WriteLine(Charles.Statement());
            Console.WriteLine(Jess.Statement());
            Console.WriteLine(Rebecca.Statement());
            Console.WriteLine(Lucas.Statement());
        }
    }
   运行后,控制台输出如下:

  接着,我们要做的就是在这个数据的基础上编写测试用例。鉴于有些同学可能还不知道怎么使用C#的单元测试框架,所以演示一次,后面若无特殊情况,一概略过。关于C#单元测试的更多使用方法,参见微软的教程,或在google上搜索。

http://msdn.microsoft.com/zh-cn/library/ms379625(v=vs.80).aspx

  1.在Statement方法上点击鼠标右键,会出现如下菜单(因为我装了VA插件,所以菜单可能不完全一致)

  2.选中其中的创建单元测试,会出现下述对话框



  3.选中待测试的Statement()方法后,点击确定

  4.在项目名称一栏,输入一个你喜欢的名字作为测试项目的名称,在这里我将测试项目命名为:Tests,然后创建该项目。解决方案中多出了下图中的文件和工程

  5.打开CustomerTest.cs文件,发现系统自动为我们添加了一个测试:
        /// <summary>
        ///Statement 的测试
        ///</summary>
        [TestMethod()]
        public void StatementTest()
        {
            string name = string.Empty; // TODO: 初始化为适当的值
            Customer target = new Customer(name); // TODO: 初始化为适当的值
            string expected = string.Empty; // TODO: 初始化为适当的值
            string actual;
            actual = target.Statement();
            Assert.AreEqual(expected, actual);
            Assert.Inconclusive("验证此测试方法的正确性。");
        }
该测试没什么用处,我们要做的是清空它,并使用前面产生的标准数据来编写第一个测试,结果参照如下:
        [TestMethod()]
        public void StatementForCharles()
        {
            Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
            Movie godFather = new Movie("GodFather", Movie.REGULAR);
            Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);

            Rental bh4DayRental = new Rental(braveHeart, 4);
            Rental bh2DRental = new Rental(braveHeart, 2);
            Rental we7DRental = new Rental(wallE, 7);
            Rental gfRental = new Rental(godFather, 5);
            Rental we3DRental = new Rental(wallE, 3);

            Customer charles = new Customer("Charles");
            charles.Add(bh4DayRental);
            charles.Add(gfRental);

            string expected = "Rental Record for Charles\n"
                            + "\tBraveHeart\t12\n"
                            + "\tGodFather\t6.5\n"
                            + "Amount owed is 18.5\n"
                            + "You earned 3 frequent renter points";

            Assert.AreEqual(expected, charles.Statement());
        }
  需要注意的是,该测试的部分代码是直接copy自上面Main函数,虽然这(拷贝、粘贴)不是被推荐的行为,但我们最终的目的实际上是将原来Main中的人工测试搬移到自动测试中,所以是可以被接受的,毕竟搬移后没有功能重复的代码。

  6.构造完该测试后,从下图所示菜单中 运行 单元测试
  短暂的编译、运行后,IDE中出现了类似下面的结果窗口:

  结果列中,绿色的小勾勾说明,我们的测试通过了,如果输入有误,则结果画面如下:
  7.初步了解怎样添加自动测试后,我们接着为另外三个参考数据编写相应的测试用例,代码如下:
        [TestMethod()]
        public void StatementForJess()
        {
            Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
            Movie godFather = new Movie("GodFather", Movie.REGULAR);
            Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);

            Rental bh4DayRental = new Rental(braveHeart, 4);
            Rental bh2DRental = new Rental(braveHeart, 2);
            Rental we7DRental = new Rental(wallE, 7);
            Rental gfRental = new Rental(godFather, 5);
            Rental we3DRental = new Rental(wallE, 3);

            Customer jess = new Customer("Jess");
            jess.Add(bh4DayRental);
            jess.Add(we7DRental);
            jess.Add(bh2DRental);

            string expected = "Rental Record for Jess\n"
                            + "\tBraveHeart\t12\n"
                            + "\tWall-E\t7.5\n"
                            + "\tBraveHeart\t6\n"
                            + "Amount owed is 25.5\n"
                            + "You earned 5 frequent renter points";

            Assert.AreEqual(expected, jess.Statement());
        }

        [TestMethod()]
        public void StatementForRebecca()
        {
            Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
            Movie godFather = new Movie("GodFather", Movie.REGULAR);
            Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);

            Rental bh4DayRental = new Rental(braveHeart, 4);
            Rental bh2DRental = new Rental(braveHeart, 2);
            Rental we7DRental = new Rental(wallE, 7);
            Rental gfRental = new Rental(godFather, 5);
            Rental we3DRental = new Rental(wallE, 3);

            Customer rebecca = new Customer("Rebecca");
            rebecca.Add(we7DRental);
            rebecca.Add(we3DRental);

            string expected = "Rental Record for Rebecca\n"
                            + "\tWall-E\t7.5\n"
                            + "\tWall-E\t1.5\n"
                            + "Amount owed is 9\n"
                            + "You earned 2 frequent renter points";

            Assert.AreEqual(expected, rebecca.Statement());
        }

        [TestMethod()]
        public void StatementForLucas()
        {
            Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
            Movie godFather = new Movie("GodFather", Movie.REGULAR);
            Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);

            Rental bh4DayRental = new Rental(braveHeart, 4);
            Rental bh2DRental = new Rental(braveHeart, 2);
            Rental we7DRental = new Rental(wallE, 7);
            Rental gfRental = new Rental(godFather, 5);
            Rental we3DRental = new Rental(wallE, 3);

            Customer lucas = new Customer("Lucas");
            lucas.Add(bh2DRental);
            lucas.Add(we7DRental);
            lucas.Add(we3DRental);

            string expected = "Rental Record for Lucas\n"
                            + "\tBraveHeart\t6\n"
                            + "\tWall-E\t7.5\n"
                            + "\tWall-E\t1.5\n"
                            + "Amount owed is 15\n"
                            + "You earned 4 frequent renter points";

            Assert.AreEqual(expected, lucas.Statement());
        }

  8.虽然测试都完成了,但是每一个测试中,有相当多的关于创建Movie和Rental对象的重复代码。本着“测试代码也要保持整洁”的原则,在这一篇的最后,我们把这些重复的可共享代码全部抽取为测试类的字段,得到完整的测试代码如下:
using CH01_MovieRentalHouse;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Tests
{
    [TestClass]
    public class CustomerTest
    {
        private Movie braveHeart;
        private Movie godFather;
        private Movie wallE;
        private Rental bh4DayRental;
        private Rental bh2DRental;
        private Rental we7DRental;
        private Rental gfRental;
        private Rental we3DRental;

        public CustomerTest()
        {
            braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
            godFather = new Movie("GodFather", Movie.REGULAR);
            wallE = new Movie("Wall-E", Movie.CHILDRENS);

            bh4DayRental = new Rental(braveHeart, 4);
            bh2DRental = new Rental(braveHeart, 2);
            we7DRental = new Rental(wallE, 7);
            gfRental = new Rental(godFather, 5);
            we3DRental = new Rental(wallE, 3);
        }

        [TestMethod]
        public void StatementForCharles()
        {
            Customer charles = new Customer("Charles");
            charles.Add(bh4DayRental);
            charles.Add(gfRental);

            string expected = "Rental Record for Charles\n"
                            + "\tBraveHeart\t12\n"
                            + "\tGodFather\t6.5\n"
                            + "Amount owed is 18.5\n"
                            + "You earned 3 frequent renter points";

            Assert.AreEqual(expected, charles.Statement());
        }

        [TestMethod]
        public void StatementForJess()
        {
            Customer jess = new Customer("Jess");
            jess.Add(bh4DayRental);
            jess.Add(we7DRental);
            jess.Add(bh2DRental);

            string expected = "Rental Record for Jess\n"
                            + "\tBraveHeart\t12\n"
                            + "\tWall-E\t7.5\n"
                            + "\tBraveHeart\t6\n"
                            + "Amount owed is 25.5\n"
                            + "You earned 5 frequent renter points";

            Assert.AreEqual(expected, jess.Statement());
        }

        [TestMethod]
        public void StatementForRebecca()
        {
            Customer rebecca = new Customer("Rebecca");
            rebecca.Add(we7DRental);
            rebecca.Add(we3DRental);

            string expected = "Rental Record for Rebecca\n"
                            + "\tWall-E\t7.5\n"
                            + "\tWall-E\t1.5\n"
                            + "Amount owed is 9\n"
                            + "You earned 2 frequent renter points";

            Assert.AreEqual(expected, rebecca.Statement());
        }

        [TestMethod]
        public void StatementForLucas()
        {
            Customer lucas = new Customer("Lucas");
            lucas.Add(bh2DRental);
            lucas.Add(we7DRental);
            lucas.Add(we3DRental);

            string expected = "Rental Record for Lucas\n"
                            + "\tBraveHeart\t6\n"
                            + "\tWall-E\t7.5\n"
                            + "\tWall-E\t1.5\n"
                            + "Amount owed is 15\n"
                            + "You earned 4 frequent renter points";

            Assert.AreEqual(expected, lucas.Statement());
        }
    }
}

  小结:这一篇并没有涉及具体的重构内容,而是一些重构前的准备:
   1.使用原始代码构造参考数据
   2.使用C#的单元测试框架为这些参考数据创建自动单元测试
  至于这么做的最重要的原因是:为后面的重构提供可自动完成的、可重复执行的检验标准

  相关代码打包下载:

特别说明 -------- 新本请访问网站www.bluefishes.net. 考虑到稳定性,新本不支持Visual Studio.NET 2002. 产品名称 -------- SharpRefactor(C#代码重构工具) 产品简述 -------- 本工具用于代码重构和代码自动生成。现阶段主要用于C#代码重构。 所谓重构也就是“保持软件的外在功能不变,重新调整其内部结构”。 关于每种重构模式的含义,请参见http://www.refactoring.com/ 具体功能参见具体本的特性列表。 对重构很感兴趣或是很关注使用效率的用户,希望[使用指南]节对你有所助益。 本 ---- 1.0.0(BETA). 发布日期 -------- 2003/6/13 作者 ---- C# Refactor Team. 制作 ---- Blue Workshop. 环境要求 -------- Visual Studio.Net 2003 Windows 2000 + SP2 + SMTP Service 特别提示 -------------- 本插件使用了异常处理和报告机制。 般而言,环境、代码以及其他原因都会导致程序出错。因此,在您使用本插件的过程中,可能会弹出错误报告。部分错误不会影响使用,另部分会影响使用。 C# Refactor Team愿意随时提供技术支持,及时为你解除问题。 本1.0.0特性 ------------- Rename Parameter Rename Local Variable Rename Field Rename Property Rename Class Rename NameSpace Safe Delete Parameter Safe Delete Local Variable Safe Delete Field Safe Delete Property Safe Delete Method Safe Delete Class Safe Delete NameSpace Extract Interface Undo/Redo Preview usage before refactor(重构前预览) Auto build after refactor(重构后自动生成) Options(工具选项) User feedback(用户反馈) 使用指南 -------- 所有功能暂不支持静态成员。 尽量使用鼠标右键菜单。 尽量使用快捷方式,比如:单击鼠标右键,弹出菜单后再连续按‘R’键和‘C’键就可以调用[Rename]菜单下的[Rename Class]命令。 在使用Rename系列命令时,需要先转到定义代码元素的地方。此时,可以先使用右键菜单中的[转到定义]命令。 在Option中可以设置首选项。 由于Visual Studio在生成较大的解决方案时有时会不成功,所以Auto build after refactor通常用于较小的解决方案。 Rename NameSpace与Move Class不同。Move Class的焦点在Class,即改变类所在的NameSpace。而Rename NameSpace的焦点在NameSpace,即改变指定NameSpace的名字,并更新该NameSpace的所有引用(Usages)。 错误报告以及建议功能需要网络连接和Windows自带的SMTP服务。因为发送速度很快,所以不会占用您宝贵的时间。 可以使用User feedback功能提出您睿智的建议、批评、任何意见。 技术支持 -------- Tiger.BlueWorkshop@163.net 下载 ---- www.youkuaiyun.com 本 发布日期 ----------------------------- 1.0.0(Beta) 2003/6/13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值