单元测试和 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。这意味着任何值。
希望这篇文章对您有所帮助!