65.9K
CodeProject 正在变化。 阅读更多。
Home

单元测试和 Rhino Mocks

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.15/5 (5投票s)

2015年12月20日

CPOL

6分钟阅读

viewsIcon

25595

downloadIcon

1

在本文中,我们将讨论单元测试和 Rhino Mocks。

引言

 在详细介绍Rhino Mocks之前,让我们先了解一下什么是单元测试?为什么需要单元测试?以及何时使用单元测试?

  •  什么是单元测试?
          编写的代码按预期工作,这就是单元测试。                
  •  为什么需要单元测试? 
           单元测试之所以需要,有几个原因:
    • 您编写的逻辑或代码是否按预期工作。
    • 单元测试将给开发人员带来信心,让他们知道自己编写的任何代码都符合业务规则。
  • 何时使用单元测试?
           如上所述,当开发人员希望验证逻辑或业务规则是否按预期工作时,就需要进行单元测试。在单元测试中,开发人员将测试所有可能的场景,并比较实际结果和预期结果是否相同。如果不相同,他/她可以采取相应的措施。

解释

太棒了!现在我们来看看如何为简单的加法功能编写测试用例。

 public class Test
        {
            public int Add(int value1,int value2)
            {
                   return value1 + value2;
            }
         }

       这是我们的测试用例。

     [TestClass]
     public class SampleUnitTest
   	   {
       	[TestMethod]
        public void AddTest_ReturnTrue()
        {
            int value1 = 7;
            int value2 = 7;
            var addTest = new Test();
            var result = addTest.Add(value1, value2);
            Assert.IsTrue(result == 14, "Result of the sum is working as expected");
        }

       [TestMethod]
        public void AddTest_ReturnFalse()
        {
            int value1 = 7;
            int value2 = 7;
            var addTest = new Test();
            var result = addTest.Add(value1, value2);
            Assert.IsFalse(result == 10, "Result of the sum is working not as expected");
        }
    }

在深入研究之前,让我们先了解一下 TestClass、TestMethod 属性和 Assert。

  •  TestClass 属性:用于标识包含测试方法的类。 
  •  TestMethod 属性:用于标识测试方法。 
  •  Assert:Assert 类用于通过真/假的前置条件来验证单元测试中的条件。

在上面的示例中,我使用了前置条件来验证条件是真还是假。 

在上面的方法中,我们使用了相同的代码。那么问题来了,使用通用方法是否更好?答案是否定的。在单元测试中,不应该关心创建可重用代码。
尽管如此,单元测试框架在一定程度上支持可重用代码。我们可以使用 TestInitialize 属性来重用代码。 

    int result;
        [TestInitialize]
        public void SetUp()
        {
            int value1 = 7;
            int value2 = 7;
            var addTest = new Test();
            var result = addTest.Add(value1, value2);
        }

让我们再看一个例子,在这个例子中我使用的是两层架构。 

 public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

 public class EmployeeDal
    {
        List employees = new List { new Employee { Id = 1, Name = "abc" }, new Employee { Id = 2, Name = "xyz" } };
        public Employee GetEmployee()
        {
            return employees.FirstOrDefault();
        }
    }

在这里,我为数据访问层创建了一个单独的项目,您可以看到,我没有直接与数据库交互。 

现在让我们看看我为业务逻辑层写了什么。

namespace EmployeeBusinessLayer
{
    public class EmployeeBal
    {
        public string GetEmployee()
        {
            return new EmployeeDal.EmployeeDal().GetEmployee().Name;
        }
    }
}

这里只是获取员工姓名。看起来很简单! 

现在让我们看看单元测试是如何编写的。

[TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Employee_ReturnEqual()
        {
            var employeeBal = new EmployeeBal();
            Assert.AreEqual(employeeBal.GetEmployee(), "abc", "Return value is not equal");
        }
    }

注意:我将在整篇文章中使用数据访问层和业务逻辑层的相同类,并对结构进行一些修改。 

看起来很简单!!但问题现在就开始了。

上面编写的代码存在各种缺点。 

  •  单元测试完全依赖于数据库(假设 EmployeeDal 访问数据库)。如果数据库发生任何变化,单元测试就会失败。 
  •  我们应该对业务逻辑层进行单元测试,而不是对数据访问层进行测试。 

现在问题出现了,如何解决上述问题?

这是解决方案。

有各种框架或测试套件可用于解决上述问题,例如:Rhino Mocks、Fakes 等。

在上面的两层示例中,代码是紧密耦合的。因此,我们必须使用依赖注入(属于 SOLID 原则)来消除这种紧密耦合。

SOLID 的每一个字母都有其意义。S -> 单一职责原则,O -> 开闭原则,L -> 里氏替换原则,I -> 接口隔离原则,D -> 依赖倒置原则(或依赖注入)。

本文主要关注单元测试和 Rhino Mocks。您可以在网上找到有关 SOLID 原则的文章。我建议您阅读这些关于 SOLID 原则的文章。
http://blog.gauffin.org/2012/05/11/solid-principles-with-real-world-examples/
https://codeproject.org.cn/Articles/703634/SOLID-architecture-principles-using-simple-Csharp -- 供初学者参考

您可能很快就会期待一篇关于 SOLID 原则的文章。祝我好运!!! 

现在我们在两层应用程序代码中使用了依赖注入。 

在深入介绍 Rhino Mocks 之前,让我们先了解一下 AAA 格式。AAA 就是 Arrange(准备)、Act(执行)和 Assert(断言)。

  • Arrange(准备):Arrange 表示设置模拟类的行为。 
  • Act(执行):Act 表示执行测试用例。
  • Assert(断言):Assert 表示验证结果。 

 在整篇文章中,您将使用 AAA 模式。 

让我们开始 Rhino Mocks 的旅程。为了进行依赖注入,我在数据访问层创建了一个新接口 IEmployeeDal。 

public interface IEmployeeDal
    {
        Employee GetEmployee();
    }
public class EmployeeDal:IEmployeeDal
    {
        List employees = new List { new Employee { Id = 1, Name = "abc" }, new Employee { Id = 2, Name = "xyz" } };
        public Employee GetEmployee()
        {
            return employees.FirstOrDefault();
        }
    }

业务逻辑层的更改如下。 

public class EmployeeBal
    {
        private IEmployeeDal _employeeDal;
        public EmployeeBal(IEmployeeDal employeeDal)
        {
            _employeeDal = employeeDal;
        }

        public Employee GetEmployee()
        {
            return _employeeDal.GetEmployee();
        }
    }

现在进入单元测试项目。 

   [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Employee_ReturnEqual()
        {
            var employeeBal = new EmployeeBal(new EmployeeDal.EmployeeDal());
            Assert.AreEqual(employeeBal.GetEmployee().Name, "abc", "Return value is not equal");
        }
    }

当您运行此应用程序时,测试会通过。但它仍然正确吗? 
我的回答是否定的,因为它仍然访问数据访问层。 

注意:在实际应用程序中,对于依赖注入,可以使用 Unity 容器、MEF、Structure Map 等。出于介绍目的,我没有使用这些容器。 

现在,让我们通过 NuGet 在测试项目中安装 Rhino Mocks。安装后,您将在引用中看到 Rhino.Mocks.dll,并在测试项目中的 package.config 文件中看到所有包信息。 

现在,让我们看看编写单元测试的正确方法。 

        [TestMethod]
        public void Employee_AssertWasCalled()
        {
            //Arrange
            var employeeDalMock = MockRepository.GenerateMock();
            var employeeBal = new EmployeeBal(employeeDalMock);

            //Act
            employeeBal.GetEmployee();

            //Assert
            employeeDalMock.AssertWasCalled(x => x.GetEmployee());
        }

当您运行此测试用例时,它会通过。 

Rhino Mocks 内部使用 Castle Dynamic Proxy 在调用 GenerateMock<T>() 时生成代理。因此,每次调用 MockRepository.GenerateMock<T>() 时都会生成代理。 

在此示例中,您可以看到正在使用 Arrange、Act 和 Assert 模式。AssertWasCalled 方法用于确定某个特定方法是否已被调用。 

注意:始终只使用 1 个 Assert 调用进行测试是一个好习惯。 

在深入介绍 Rhino Mocks 之前,让我们先了解一下 Rhino Mocks 的选项。 

  • Strict Mocks(严格模拟):这是一种模拟,如果任何方法没有被明确设置为使用,它将抛出异常。通常,我们使用严格模拟来验证所有异常。
  • Dynamic Mock(动态模拟):这是一种模拟,如果任何方法未被设置,它不会抛出任何异常,并且将为该方法返回默认值 null。在上面的示例中,我们已经使用了模拟机制。 
  • PartialMocks(部分模拟):部分模拟也类似于动态模拟,它不会抛出任何异常。但是,它不会像动态模拟那样发送默认值 null,而是返回实现类的实际值。我们主要在访问抽象类时使用部分模拟。您可以在附带的源代码中找到部分模拟的来源。 

让我们再举一个如何使用 stub(存根)的例子。 

  public interface IEmployeeDal
    {
        Employee GetEmployee();

        string GetEmployeeById(int id);
    }

   public class EmployeeDal:IEmployeeDal
    {
        List employees = new List { new Employee { Id = 1, Name = "abc" }, new Employee { Id = 2, Name = "xyz" } };
        public Employee GetEmployee()
        {
            return employees.FirstOrDefault();
        }

        public string GetEmployeeById(int id)
        {
            return employees.FirstOrDefault(x => x.Id == id).Name;
        }
    }

    public class EmployeeBal
    {
        private IEmployeeDal _employeeDal;
        public EmployeeBal(IEmployeeDal employeeDal)
        {
            _employeeDal = employeeDal;
        }

        public Employee GetEmployee()
        {
            return _employeeDal.GetEmployee();
        }

        public string GetEmployeeById(int id)
        {
            return _employeeDal.GetEmployeeById(id);
        }
    }

        [TestMethod]
        public void Employee_AreEqual()
        {
            //Arrange
            var employeeDalMock = MockRepository.GenerateMock();
            employeeDalMock.Stub(x => x.GetEmployeeById(1)).Return("abc");
            var employeeBal = new EmployeeBal(employeeDalMock);

            //Act
            var result = employeeBal.GetEmployeeById(1);

            //Assert
            Assert.AreEqual(result, "abc", "value is not abc");
        }

在这个示例中,我们告诉 Rhino Mocks,当在数据访问层中调用 GetEmployeeById 方法时,返回值应为“abc”。 

在上面的示例中,您可以看到我传递的 Id 值是 1。Id=1 不是必需的,因为我们告诉 Rhino Mocks 无论什么值都返回“abc”,但仍然必须传递一个整数值。对吗? 
这就是为什么 Rhino Mocks 引入了 Rhino Mock Constraints(约束)这一功能。 

现在看看我们如何修改它。 

        [TestMethod]
        public void Employee_AreEqual()
        {
            //Arrange
            var employeeDalMock = MockRepository.GenerateMock();
            employeeDalMock.Stub(x => x.GetEmployeeById(Arg.Is.Anything)).Return("abc");
            var employeeBal = new EmployeeBal(employeeDalMock);

            //Act
            var result = employeeBal.GetEmployeeById(Arg.Is.Anything);

            //Assert
            Assert.AreEqual(result, "abc", "value is not abc");
        }

而不是 Id 作为 1,我们使用的是 Arg<int>.Is.Anything。这意味着任何值。 

希望这篇文章对您有所帮助! 

© . All rights reserved.