使用 NUnit、 Rhino Mocks 和 Entity Framework 的 WCF 服务单元测试






4.83/5 (3投票s)
如何使用 NUnit 框架为我们的 WCF 服务编写单元测试用例
引言
在本帖中,我们将学习如何使用一个名为 NUnit 的框架为我们的 WCF 服务 编写单元测试用例。我们还将介绍如何在测试中模拟我们的依赖项,在这里我们将使用 Rhino Mocks。我将使用 Visual Studio 2015 进行开发。希望您会喜欢这篇文章。
背景
作为开发人员,我们每天都要编写大量的代码。我说的对吗?检查我们编写的代码是否正常工作非常重要。因此,为此,我们开发人员通常会进行单元测试,一些开发人员会进行手动测试来检查功能是否正常工作。我认为这是错误的。在 TDD(测试驱动开发)中,单元测试非常重要,我们实际上是在开始编码之前编写测试用例。让我们看看“单元测试”究竟是什么。
单元测试
单元测试是测试一个单元的过程,它可以是一个类、一段代码、一个函数、一个属性。我们可以轻松地独立测试我们的单元。在 .NET 中,我们有很多框架可以进行单元测试。但在这里,我们将使用 NUnit,我发现它非常容易编写测试。
如果您在机器上安装了 Resharper,那么执行和调试测试将更加容易。在这里,我在我的 Visual Studio 中使用 Resharper,所以屏幕截图将基于此。谢谢。
现在是时候设置我们的项目并开始编码了。
设置项目
要开始,请在您的 Visual Studio 中创建一个空项目。
现在,我们将添加一个 WCF 服务,如下所示:

完成后,您将看到两个文件,一个接口(IMyService
)和一个类(MyService
),它们带有 .svc 扩展名。如果您是 WCF 服务的新手,我强烈建议您在此处阅读一些基础知识 。
现在,是时候设置我们的数据库并插入一些数据了。
创建数据库
在这里,我创建了一个名为 TrialDB
的数据库,您可以通过运行下面的查询来创建 DB。
USE [master]
GO
/****** Object: Database [TrialDB] Script Date: 20-11-2016 03:54:53 PM ******/
CREATE DATABASE [TrialDB]
CONTAINMENT = NONE
ON PRIMARY
( NAME = N'TrialDB', _
FILENAME = N'C:\Program Files\Microsoft SQL Server\_
MSSQL13.SQLEXPRESS\MSSQL\DATA\TrialDB.mdf' , SIZE = 8192KB , _
MAXSIZE = UNLIMITED, FILEGROWTH = 65536KB )
LOG ON
( NAME = N'TrialDB_log', _
FILENAME = N'C:\Program Files\Microsoft SQL Server\_
MSSQL13.SQLEXPRESS\MSSQL\DATA\TrialDB_log.ldf' , SIZE = 8192KB , _
MAXSIZE = 2048GB , FILEGROWTH = 65536KB )
GO
ALTER DATABASE [TrialDB] SET COMPATIBILITY_LEVEL = 130
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [TrialDB].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [TrialDB] SET ANSI_NULL_DEFAULT OFF
GO
ALTER DATABASE [TrialDB] SET ANSI_NULLS OFF
GO
ALTER DATABASE [TrialDB] SET ANSI_PADDING OFF
GO
ALTER DATABASE [TrialDB] SET ANSI_WARNINGS OFF
GO
ALTER DATABASE [TrialDB] SET ARITHABORT OFF
GO
ALTER DATABASE [TrialDB] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [TrialDB] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [TrialDB] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [TrialDB] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [TrialDB] SET CURSOR_DEFAULT GLOBAL
GO
ALTER DATABASE [TrialDB] SET CONCAT_NULL_YIELDS_NULL OFF
GO
ALTER DATABASE [TrialDB] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [TrialDB] SET QUOTED_IDENTIFIER OFF
GO
ALTER DATABASE [TrialDB] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [TrialDB] SET DISABLE_BROKER
GO
ALTER DATABASE [TrialDB] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO
ALTER DATABASE [TrialDB] SET DATE_CORRELATION_OPTIMIZATION OFF
GO
ALTER DATABASE [TrialDB] SET TRUSTWORTHY OFF
GO
ALTER DATABASE [TrialDB] SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE [TrialDB] SET PARAMETERIZATION SIMPLE
GO
ALTER DATABASE [TrialDB] SET READ_COMMITTED_SNAPSHOT OFF
GO
ALTER DATABASE [TrialDB] SET HONOR_BROKER_PRIORITY OFF
GO
ALTER DATABASE [TrialDB] SET RECOVERY SIMPLE
GO
ALTER DATABASE [TrialDB] SET MULTI_USER
GO
ALTER DATABASE [TrialDB] SET PAGE_VERIFY CHECKSUM
GO
ALTER DATABASE [TrialDB] SET DB_CHAINING OFF
GO
ALTER DATABASE [TrialDB] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF )
GO
ALTER DATABASE [TrialDB] SET TARGET_RECOVERY_TIME = 60 SECONDS
GO
ALTER DATABASE [TrialDB] SET DELAYED_DURABILITY = DISABLED
GO
ALTER DATABASE [TrialDB] SET QUERY_STORE = OFF
GO
USE [TrialDB]
GO
ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP = 0;
GO
ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET MAXDOP = PRIMARY;
GO
ALTER DATABASE SCOPED CONFIGURATION SET LEGACY_CARDINALITY_ESTIMATION = OFF;
GO
ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY _
SET LEGACY_CARDINALITY_ESTIMATION = PRIMARY;
GO
ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = ON;
GO
ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY _
SET PARAMETER_SNIFFING = PRIMARY;
GO
ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES = OFF;
GO
ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY _
SET QUERY_OPTIMIZER_HOTFIXES = PRIMARY;
GO
ALTER DATABASE [TrialDB] SET READ_WRITE
GO
创建数据库表并插入数据
要创建表,您可以运行以下查询。
USE [TrialDB]
GO
/****** Object: Table [dbo].[Course]
Script Date: 20-11-2016 03:57:30 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Course](
[CourseID] [int] NOT NULL,
[CourseName] [nvarchar](50) NOT NULL,
[CourseDescription] [nvarchar](100) NULL,
CONSTRAINT [PK_Course] PRIMARY KEY CLUSTERED
(
[CourseID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, _
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
现在我们可以向我们新创建的表中插入一些数据。
USE [TrialDB]
GO
INSERT INTO [dbo].[Course]
([CourseID]
,[CourseName]
,[CourseDescription])
VALUES
(1
,'C#'
,'Learn C# in 7 days')
INSERT INTO [dbo].[Course]
([CourseID]
,[CourseName]
,[CourseDescription])
VALUES
(2
,'Asp.Net'
,'Learn Asp.Net in 7 days')
INSERT INTO [dbo].[Course]
([CourseID]
,[CourseName]
,[CourseDescription])
VALUES
(3
,'SQL'
,'Learn SQL in 7 days')
INSERT INTO [dbo].[Course]
([CourseID]
,[CourseName]
,[CourseDescription])
VALUES
(4
,'JavaScript'
,'Learn JavaScript in 7 days')
GO
所以我们的数据已准备好,这意味着我们已准备好编写我们的服务和测试。现在转到您的解决方案并创建一个实体数据模型。
所以实体也已创建。现在请打开您的接口,这就是我们将开始编码的地方。我们可以如下修改接口:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WCF_NUnit_Tests_Rheno_Mocks
{
[ServiceContract]
public interface IMyService
{
[OperationContract]
Course GetCourseById(int courseId);
[OperationContract]
List<Course> GetAllCourses();
}
}
在这里,我们创建了两个操作,一个是通过 ID 获取课程,另一个是将所有课程作为列表检索。现在请在我们的服务文件中实现这两个操作。您可以按如下方式修改该类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WCF_NUnit_Tests_Rheno_Mocks
{
public class MyService : IMyService
{
private static MyEntity _myContext;
private static IMyService _myIService;
public MyService()
{
}
public MyService(IMyService myIService)
{
_myContext = new MyEntity();
_myIService = myIService;
}
public Course GetCourseById(int courseId)
{
var crse = _myContext.Courses.FirstOrDefault_
(dt => dt.CourseID == courseId);
return crse;
}
public List<Course> GetAllCourses()
{
var courses = (from dt in _myContext.Courses select dt).ToList();
return courses;
}
}
}
在上面的代码中,您可以看到,我们创建了两个构造函数,一个是没有参数的,另一个是有参数的,并且我们以 IMyService
作为参数。这样,我们在编写单元测试时就可以实现依赖项注入。所以我们所要做的就是传递依赖项,在本例中是 IMyService
。
在软件工程中,依赖项注入是一种软件设计模式,它通过控制反转来解决依赖关系。依赖项是可以使用(一个服务)的对象。注入是将依赖项传递给会使用它的依赖对象(一个客户端)。
来源:WikiPedia
如果您想了解更多关于依赖项注入的信息,请在此处阅读 。现在我们将构建并检查我们的服务是否正常运行。请按 CTRL+F5。
由于我们的服务已准备好,我们现在可以为这些操作创建测试。为此,我们可以为我们的项目创建一个新的类库,并将其命名为 UnitTest.Service
。请在类库中添加一个名为 MyServiceTests
的类,我们可以在其中添加我们的测试。并且请不要忘记添加我们的应用程序引用。
安装和配置 NUnit
现在我们可以从 NuGet 包安装 NUnit 到我们的测试项目。添加包后,您将能够在我们的 MyServiceTests
类中添加前面的命名空间。
using NUnit.Framework;
在 NUnit 中,我们有许多可用于不同目的的属性,但现在我们只使用其中的四个。
TestFixture
testfixture_in_nunit-
OneTimeSetUp
one_time_setup_attribute_in_nunit在早期版本中,我们使用了
TestFixtureSetUp
,由于TestFixtureSetUp
已被弃用,现在我们使用OneTimeSetUp
。testfixturesetup_attribute_is_obsolete -
TearDown
此属性用于标识在每个测试之后立即调用的方法,即使发生任何错误也会调用它,我们可以在这里处置我们的对象。
测试
此属性用于使方法可以从 NUnit 测试运行器调用。此属性不能被继承。
现在我们可以看到所有这些属性都在起作用。所以让我们写一些测试,但真正的问题是我们确实需要模拟 IMyService
,因为 MyService
类的参数化构造函数期望它。还记得我们讨论过如何设置我们的服务以便注入依赖项吗?不用担心,我们现在可以安装 Rhino Mock 来做到这一点。
所以我们可以像这样在我们的测试类中添加测试作为依赖项:
using NUnit.Framework;
using Rhino.Mocks;
using WCF_NUnit_Tests_Rhino_Mocks;
namespace UnitTest.Service
{
[TestFixture]
public class MyServiceTests
{
private static MyService _myService;
private IMyService _myIservice;
[OneTimeSetUp]
public void SetUp()
{
_myIservice = MockRepository.GenerateMock<IMyService>();
_myService = new MyService(_myIservice);
}
[TearDown]
public void Clean()
{
}
[Test(Description = "A test to check whether the returned value is null")]
public void GetCourseById_Return_NotNull_Pass()
{
//Set Up
var crs = new Course
{
CourseID = 1,
CourseName = "C#",
CourseDescription = "Learn course in 7 days"
};
_myIservice.Stub(dt =>
dt.GetCourseById(1)).IgnoreArguments().Return(crs);
//Act
crs = _myService.GetCourseById(1);
//Assert
Assert.IsNotNull(crs,"The returned value is null");
}
[Test(Description = "A test to check we get all the courses")]
public void GetAllCourses_Return_List_Count_Pass()
{
//Act
var crs = _myService.GetAllCourses();
//Assert
Assert.AreEqual(4, crs.Count,
"The count of retrieved data doesn't match");
_myIservice.VerifyAllExpectations();
}
}
}
正如您所见,我们已按如下方式模拟了我们的 IMyService
。
_myIservice = MockRepository.GenerateMock<IMyService>();
并且,在测试 GetCourseById_Return_NotNull_Pass
中,我们还使用了一个名为 Stub
的方法。Stub
实际上告诉 mock 对象在调用匹配方法时执行某个操作,它不会为此创建预期。所以您可能会想,我们如何创建预期?为此,我们有一个名为 Expect
的方法。
当您使用 Expect
时,建议始终验证您的预期,就像我们在测试 GetAllCourses_Return_List_Count_Pass
中使用它一样。
_myIservice.VerifyAllExpectations();
正如我之前所说,我使用的是 Resharper,我们有很多运行测试的快捷方式,现在如果您右键单击您的 TestFixture
。您可以看到一个运行全部选项,如下所示:
当我在运行测试时收到“在应用程序配置文件中找不到名为 '
Entity
' 的连接字符串。”的错误时,我不得不将实体框架安装到我的测试项目中,并且还添加了一个新的配置文件,其中包含与我们的 web 配置文件中的连接字符串相同的连接字符串。
如果一切顺利,并且您没有任何错误,我相信您会看到一个屏幕,如下所示:
祝您编码愉快!
另请参阅
结论
您觉得我遗漏了什么吗?您觉得这篇帖子有用吗?我希望您喜欢这篇文章。请分享您宝贵的建议和反馈。
现在轮到你了。你有什么想法?
没有评论的博客不是博客,但请尽量保持主题。如果您有一个与本帖无关的问题,最好将其发布到 C# Corner、CodeProject、Stack Overflow、ASP.NET 论坛,而不是在此处评论。在 Twitter 或电子邮件中给我您问题的链接,如果我可以,我一定会尽力提供帮助。