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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (3投票s)

2016年11月20日

CPOL

6分钟阅读

viewsIcon

13150

如何使用 NUnit 框架为我们的 WCF 服务编写单元测试用例

引言

在本帖中,我们将学习如何使用一个名为 NUnit 的框架为我们的 WCF 服务 编写单元测试用例。我们还将介绍如何在测试中模拟我们的依赖项,在这里我们将使用 Rhino Mocks。我将使用 Visual Studio 2015 进行开发。希望您会喜欢这篇文章。

背景

作为开发人员,我们每天都要编写大量的代码。我说的对吗?检查我们编写的代码是否正常工作非常重要。因此,为此,我们开发人员通常会进行单元测试,一些开发人员会进行手动测试来检查功能是否正常工作。我认为这是错误的。在 TDD(测试驱动开发)中,单元测试非常重要,我们实际上是在开始编码之前编写测试用例。让我们看看“单元测试”究竟是什么。

单元测试

单元测试是测试一个单元的过程,它可以是一个类、一段代码、一个函数、一个属性。我们可以轻松地独立测试我们的单元。在 .NET 中,我们有很多框架可以进行单元测试。但在这里,我们将使用 NUnit,我发现它非常容易编写测试。

如果您在机器上安装了 Resharper,那么执行和调试测试将更加容易。在这里,我在我的 Visual Studio 中使用 Resharper,所以屏幕截图将基于此。谢谢。

现在是时候设置我们的项目并开始编码了。

设置项目

要开始,请在您的 Visual Studio 中创建一个空项目。

empty_project

empty_project

现在,我们将添加一个 WCF 服务,如下所示:

create_a_wcf_service
create_a_wcf_service

完成后,您将看到两个文件,一个接口(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

所以我们的数据已准备好,这意味着我们已准备好编写我们的服务和测试。现在转到您的解决方案并创建一个实体数据模型。

entity_framework

entity_framework

所以实体也已创建。现在请打开您的接口,这就是我们将开始编码的地方。我们可以如下修改接口:

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

invoking_wcf_service

invoking_wcf_service

由于我们的服务已准备好,我们现在可以为这些操作创建测试。为此,我们可以为我们的项目创建一个新的类库,并将其命名为 UnitTest.Service。请在类库中添加一个名为 MyServiceTests 的类,我们可以在其中添加我们的测试。并且请不要忘记添加我们的应用程序引用。

add_project_reference

add_project_reference

安装和配置 NUnit

现在我们可以从 NuGet 包安装 NUnit 到我们的测试项目。添加包后,您将能够在我们的 MyServiceTests 类中添加前面的命名空间。

using NUnit.Framework;

在 NUnit 中,我们有许多可用于不同目的的属性,但现在我们只使用其中的四个。

  • TestFixture

    testfixture_in_nunit

    testfixture_in_nunit
  • OneTimeSetUp

    one_time_setup_attribute_in_nunit

    one_time_setup_attribute_in_nunit

    在早期版本中,我们使用了 TestFixtureSetUp,由于 TestFixtureSetUp 已被弃用,现在我们使用 OneTimeSetUp

    testfixturesetup_attribute_is_obsolete

    testfixturesetup_attribute_is_obsolete
  • TearDown

    此属性用于标识在每个测试之后立即调用的方法,即使发生任何错误也会调用它,我们可以在这里处置我们的对象。

  • 测试

    此属性用于使方法可以从 NUnit 测试运行器调用。此属性不能被继承。

现在我们可以看到所有这些属性都在起作用。所以让我们写一些测试,但真正的问题是我们确实需要模拟 IMyService,因为 MyService 类的参数化构造函数期望它。还记得我们讨论过如何设置我们的服务以便注入依赖项吗?不用担心,我们现在可以安装 Rhino Mock 来做到这一点。

rhino_mocks_in_nuget_package

rhino_mocks_in_nuget_package

所以我们可以像这样在我们的测试类中添加测试作为依赖项:

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>();

generate_mock_with_rhino

generate_mock_with_rhino

并且,在测试 GetCourseById_Return_NotNull_Pass 中,我们还使用了一个名为 Stub 的方法。Stub 实际上告诉 mock 对象在调用匹配方法时执行某个操作,它不会为此创建预期。所以您可能会想,我们如何创建预期?为此,我们有一个名为 Expect 的方法。

expect_in_rhino_mock

expect_in_rhino_mock

当您使用 Expect 时,建议始终验证您的预期,就像我们在测试 GetAllCourses_Return_List_Count_Pass 中使用它一样。

_myIservice.VerifyAllExpectations();

正如我之前所说,我使用的是 Resharper,我们有很多运行测试的快捷方式,现在如果您右键单击您的 TestFixture。您可以看到一个运行全部选项,如下所示:

run_all_test_option_in_resharper

run_all_test_option_in_resharper

当我在运行测试时收到“在应用程序配置文件中找不到名为 'Entity' 的连接字符串。”的错误时,我不得不将实体框架安装到我的测试项目中,并且还添加了一个新的配置文件,其中包含与我们的 web 配置文件中的连接字符串相同的连接字符串。

如果一切顺利,并且您没有任何错误,我相信您会看到一个屏幕,如下所示:

nunit_output

nunit_output

祝您编码愉快!

另请参阅

结论

您觉得我遗漏了什么吗?您觉得这篇帖子有用吗?我希望您喜欢这篇文章。请分享您宝贵的建议和反馈。

现在轮到你了。你有什么想法?

没有评论的博客不是博客,但请尽量保持主题。如果您有一个与本帖无关的问题,最好将其发布到 C# Corner、CodeProject、Stack Overflow、ASP.NET 论坛,而不是在此处评论。在 Twitter 或电子邮件中给我您问题的链接,如果我可以,我一定会尽力提供帮助。

© . All rights reserved.