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

本文介绍如何为复杂的C#代码进行重构前的准备工作,包括构造参考数据和使用单元测试框架创建自动测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先,列出的是第一个“电影租赁”案例的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#的单元测试框架为这些参考数据创建自动单元测试
  至于这么做的最重要的原因是:为后面的重构提供可自动完成的、可重复执行的检验标准

  相关代码打包下载:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值