单元测试和 Rhino Mocks






4.15/5 (5投票s)
在本文中,我们将讨论单元测试和 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。这意味着任何值。
希望这篇文章对您有所帮助!


