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

MOQ 和 C#(包括同步和异步 WCF 服务调用)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (7投票s)

2011年4月1日

CPOL

7分钟阅读

viewsIcon

46986

downloadIcon

814

使用 MOQ 模拟您的 .NET 服务(包括异步调用)或类库

引言

在对代码进行“单元测试”时,您会希望一次测试少量代码(即,每个类中的每个方法)。如果您知道每个方法都按预期工作,这将提高整个项目的稳定性。在测试方法的功能时,您会希望排除对数据库、第三方 DLL 或您自己的服务的外部调用,以便更好地“黑盒测试”您的应用程序。要从代码中排除这些外部资源并能够用您自己的方法调用返回替换这些方法调用返回,您需要模拟您的测试。有许多编写精良的模拟可用,但 MOQ 是一种易于快速上手的方法,这对于开发人员进行单元测试来说是一个巨大的优势。

背景

什么是模拟

模拟对象是在受控环境中模拟真实对象行为的实例化对象,从而提供知情(预期)结果。

为什么要模拟

最好将库设计为具有最少的依赖项,但有时这并不实用,尤其是在设计库时没有考虑可测试性的情况下。在对具有复杂依赖项的库进行单元测试时,这些依赖项可能需要困难或耗时的设置过程,这不利于测试本身。您正在进行单元测试的代码很可能依赖于尚未完全测试或最终设计的依赖项。但这不应该阻止开发人员测试他们的新过程代码——因此,您将模拟此(尚未测试的)依赖项的返回并测试您的代码是否按预期运行。模拟还为您提供了更好的代码覆盖率——使用 Ncover 等工具可以极大地提高团队的生产力,因为您可以查看哪些代码路径已经过测试以及哪些路径仍然需要更多测试——因此,模拟可以通过让您实现通常需要更多设置(调用服务或用特定数据填充数据库表)来执行测试的路径,从而极大地增强您的代码覆盖率。

当正在进行单元测试的方法/对象具有以下特性时,模拟变得(必要地)有利

  • 具有难以创建或重现的状态(例如,网络错误、数据库服务器宕机)
  • 速度慢(例如,对多个服务进行多次调用,这些服务必须在测试之前进行初始化)
  • 尚不存在或可能会改变行为
  • 必须专门为测试目的(而不是为了实际任务)包含信息和方法

模拟测试和集成测试之间的区别

基本上,通过集成测试,您正在对系统进行端到端测试,以查看其作为一个整体系统是否按预期运行。但是通过模拟,您正在查看您的小代码片段(函数)是否按预期工作。因此,可以说,集成测试由许多更小的代码单元组成(您已经单独模拟了这些代码单元)。

Using the Code

Visual Studio 项目结构

在图 1 中,您可以看到我为解决方案创建的结构。我有一个类定义、几个类库(MainApp 将等同于您应用程序中的 MainContext 类)。一组测试项目,最后是一个 WCF 服务。

MoqAndDotNet/ProjectStructure.png

图 1

图 2 显示了一个展开的解决方案。“MoqDllTests”项目的目的是模拟对“MainApp”项目中使用的 DLL(DatabaseLibFileLib)方法的调用。第二个测试项目“MoqServiceTests”将模拟从该测试项目中的“ViewModel”类(该类“ViewModel”将像一个 Context 类)内部调用 WCF 服务“MoqService”。

MoqAndDotNet/ProjectExpanded.png

图 2

Pet 类

下面的 Pet 类只是一个“普通的旧类对象”(POCO),带有一些 private 属性、构造函数、枚举以及设置器和获取器。该类用于从(外部)服务或类库中传回信息,因此在某些情况下,其内容将被模拟。

  [Serializable]
    public class Pet
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Pets"/> class.
        /// </summary>
        public Pet(string name, int age, AnimalType typeOfAnimal, List<Food> canEat)
        {
            this.Age = age;
            this.CanEat = canEat;
            this.TypeOfAnimal = typeOfAnimal;
            this.Name = name;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Pet"/> class.
        /// </summary>
        public Pet() { }

        // setters & Getters
        public string Name { set; get; }        
        public int Age { set; get; }
        public AnimalType TypeOfAnimal { set; get; }
        public List<Food> CanEat { set; get; }

        [Serializable]
        public enum Food
        {
            Hay,
            Chum,
            Milk
        }

        [Serializable]
        public enum AnimalType
        {            
            Dog,
            Cat,
            Horse,
            Rabbit
        }
    }

主类

下面是主应用程序的代码——例如,在您的情况下,可能是启动页面。我基本上想测试下面的这些方法是否正常工作,但如果它们超出此测试的范围(即,调用数据库或 DLL——它们都以某种形式进行调用),我不想测试它们调用的方法。

public class Context
    {
        #region Class Variables
        
        public IFileController MyFile { get; set; } // File IO layer
        public IDatabaseController MyDatabase { get; set; } // Database layer
        public IPetsController MyPetService { get; set; } // Wcf service

        #endregion

        #region Constructors
        
        /// <summary>
        /// Initializes a new instance of the <see cref="Context"/> class.
        /// </summary>
        /// <param name="fileService">The file service.</param>
        /// <param name="databaseService">The database service.</param>
        /// <param name="petService">The pet service.</param>
        public Context(IFileController fileService, IDatabaseController databaseService, 
            IPetsController petService)
        {
            this.MyFile = fileService;
            this.MyDatabase = databaseService;
            this.MyPetService = petService;            
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Context"/> class.
        /// </summary>
        public Context()
        {
            this.MyFile = new FileController();
            this.MyDatabase = new DatabaseController();
            this.MyPetService = new PetsService.PetsControllerClient();
        }

        #endregion

        #region Class Methods

        /// <summary>
        /// Database layer call
        /// </summary>
        /// <returns></returns>
        public int DoubleNumberOfKennelsInDatabase()
        {            
            return this.MyDatabase.NumberOfKennelsInDatabase() * 2;             
        }

        /// <summary>
        /// File IO layer call
        /// </summary>
        /// <returns></returns>
        public int TrebleNumberOfKennelsOnFile()
        {
            return this.MyFile.NumberOfKennelsOnFile() * 3; 
        }

        /// <summary>
        /// Wcf Service call
        /// </summary>
        /// <returns></returns>
        public int QuadrupalNumberOfKennelsOnWcfService()
        {
            return this.MyPetService.NumberOfDogs() * 4;
        }

        #endregion
       
    }

下面是数据库访问层的接口声明和数据库层本身的实现。我只使用了两个简单的方法,它们被上面的 (Context) 类使用。请注意,我重载了构造函数,使其成为一个正常的空参数,并接受对外部 DLL 或引用的引用,这个构造函数是我能够模拟此类中方法的主要原因——这意味着我能够将模拟引用注入到数据库层类中,但不会干扰层中方法的工作。

 namespace DatabaseLib
{
    public interface IDatabaseController
    {
        int NumberOfKennelsInDatabase();
        DateTime ClosingTimeKennels();
    }
}
 namespace DatabaseLib
{
    public class DatabaseController : IDatabaseController
    {
         public IPetsController MyPetService { get; set; } // Wcf service

        public DatabaseController() { }
        public DatabaseController(IPetsController petService) { 
            this.MyPetService = petService; }

        public int NumberOfKennelsInDatabase()
        {
            // makes connection to database...etc
            return 10;
        }

        /// <summary>
        /// Current time plus two hour will be returned from the service .
        /// </summary>
        /// <returns></returns>
        public DateTime ClosingTimeKennels()
        {            
            return MyPetService.GetCurrentDateTime().AddHours(1);
        }
    }
}

单元测试和设置模拟对象

下面的代码片段展示了我如何在测试类中设置模拟实例。我计划模拟 IDatabaseIFileMoqService 对象。这样,当调用某些(模拟的)方法时,它们将不会访问数据库、文件系统或 Web 服务,而只是返回我指示它们返回的内容。

请注意,在 Init() 方法中,我创建了一些虚拟数据(Pet 类),这些数据将用于模拟方法返回的预期数据。这是“黑盒测试”,比较返回的内容与预期的内容。

#region Class Variables
        
        Mock<IDatabaseController> moqDB;
        Mock<IFileController> moqFile;
        Mock<ProjectCode.PetsService.IPetsController> petService;
        Context context;        
        List<Pets.Pet> lstPets;

        #endregion

        #region Constructor
        
        public ContextTests(){}

        #endregion

        #region Setup

        [TestFixtureSetUp]
        public void Init()
        {
            moqDB = new Mock<IDatabaseController>();
            moqFile = new Mock<IFileController>();
            petService = new Mock<ProjectCode.PetsService.IPetsController>();            
            context = new Context(moqFile.Object, moqDB.Object, petService.Object);

            lstPets = new List<Pets.Pet>();
            var pet = new Mock<Pets.Pet>(MockBehavior.Strict).SetupAllProperties();
            var petStub = pet.Object;

            petStub.Age = 20;
            petStub.CanEat = new List<Pets.Pet.Food>();
            petStub.CanEat.Add(Pets.Pet.Food.Milk);
            petStub.Name = "TopCat";
            petStub.TypeOfAnimal = Pets.Pet.AnimalType.Cat;
            lstPets.Add(petStub);

            petStub.Age = 10;
            petStub.CanEat.Clear();
            petStub.CanEat.Add(Pets.Pet.Food.Chum);
            petStub.CanEat.Add(Pets.Pet.Food.Milk);
            petStub.Name = "TopDog";
            petStub.TypeOfAnimal = Pets.Pet.AnimalType.Dog;
            lstPets.Add(petStub);
        }

#endregion

对类库的模拟调用

我希望确保数据库类方法“NumberOfKennelsInDatabase()”在“MainContext”类中的方法“DoubleNumberOfKennels()”执行时返回一个特定的数字。在示例代码中,我告诉方法“NumberOfKennelsInDatabase()”在每次被调用时返回 25。“DoubleNumberOfKennels()”所做的只是将返回值加倍,因此我知道要对从“DoubleNumberOfKennels()”返回的值进行断言。

请记住,我们正在测试“Context”类中的代码,因此我们正在模拟数据库类(从中提取一层)中的代码。

/// <summary>
/// A mocking of a database call within the database dll         
/// </summary>
[Test(Description = "Mocking a Context class method that makes a db call operation.")]
public void TestMockDBCall()
{
    moqDB.Setup(db => db.NumberOfKennelsInDatabase()).Returns(25);            

    int retValue = context.DoubleNumberOfKennelsInDatabase();
    Assert.IsTrue(retValue == 50.0);
}

模拟预期异常

开发人员需要模拟某些已知异常并测试其代码如何处理这些异常。因此,您能够将模拟异常抛回给调用类,从而测试您的代码如何处理模拟的结果。

[Test(Description = "Test for an expected thrown exception")]
[ExpectedException(typeof(ArgumentException))]
public void TestThrownExceptionNumberOfDogs()
{
    // exception message
    const string exceptionMessage = "Non-Initialised Object On Server.";
 
    Mock<IPetsController> petServiceTests = new Mock<IPetsController>();
  
    // setup (mock) the test
    petServiceTests.Setup<int>(method => method.NumberOfDogs()).Throws(
                new ArgumentException(exceptionMessage)); 
    IPetsController value = petServiceTests.Object; // pass mock instance to interface
 
    int returns = value.NumberOfDogs(); // call mock (no int value returned)
 
    #region Alternative code
    //try
    //{
    //    int returns = value.NumberOfDogs(); // call mock
    //}
    //catch (Exception ex)
    //{
    //    // test exception is as expected
    //    Assert.IsTrue(ex.Message.Equals(exceptionMessage)); 
    //}                     
    #endregion
}

模拟同步调用 (MoqServiceTests 项目)

在第二个测试项目中,我执行同步和异步测试,在下面的代码中,有一个异步方法及其对应的完成方法。我想测试我是否可以单独调用它们,然后我是否可以调用“Begin”方法并测试“End”方法中返回的内容。

为此,我创建了一个新类 Context,名为“ViewModel”。这个类将是我的主类,它调用下面的服务方法。因此,我想测试“ViewModel”中的这些调用方法并模拟服务调用。

private IDatabaseController iDatabaseController;
public PetsController() { iDatabaseController = new DatabaseController(); }
public PetsController(IDatabaseController databaseService) {
            iDatabaseController = databaseService; }

#region Async. Method
        
/// <summary>
/// Begins the get all names by age.
/// </summary>
/// <param name="age">The age.</param>
/// <param name="callback">The callback.</param>
/// <param name="asyncState">State of the async.</param>
/// <returns></returns>
public IAsyncResult BeginGetAllPetsByAge(int age, AsyncCallback callback,
            object asyncState)
{      
    Pet pet = new Pet();
    List<Pets.Pet> listPets = new List<Pets.Pet>();

    // PERFORM THE WORKING OUT HERE            
    pet.Age = 10;
    pet.CanEat.Add(Pet.Food.Hay);
    pet.Name = "Jumper";
    pet.TypeOfAnimal = Pet.AnimalType.Horse;
    listPets.Add(pet);

    return new CompletedAsyncResult<List<Pets.Pet>>(listPets);
}

/// <summary>
/// This method is called by the BEGIN method, it does not need to 
/// be called by the client.
/// </summary>
/// <param name="r">Result of processing</param>
/// <returns></returns>
public List<Pets.Pet> EndGetAllPetsByAge(IAsyncResult r)
{
    CompletedAsyncResult<List<Pets.Pet>> result = 
                r as CompletedAsyncResult<List<Pets.Pet>>;
    return result.Data;
}

/// <summary>
/// Tests the begin async.
/// </summary>
[Test]
public void TestBeginAsync()
{
    AsyncCallback callback = new AsyncCallback(CompletePets);
    Mock<IAsyncResult> mockAsyncResult = new Mock<IAsyncResult>();
    IAsyncResult expectedAasyncResult;
    Mock<IPetsController> petServiceTests = new Mock<IPetsController>();
            
    petServiceTests.Setup(async => async.BeginGetAllPetsByAge(It.IsAny<int>(), 
                It.IsAny<AsyncCallback>(),
                It.IsAny<object>())).Returns(mockAsyncResult.Object);
    IPetsController value = petServiceTests.Object; 	// pass mock instance 
							// to interface
    expectedAasyncResult = value.BeginGetAllPetsByAge(10, callback, new object());    

    // using the callback to test that a method was indeed called.
    Assert.IsTrue(expectedAasyncResult == mockAsyncResult.Object); 
}

/// <summary>
/// Tests the end async.
/// </summary>
[Test]
public void TestEndAsync()
{
    List<Pets.Pet> expectedResult = new List<Pets.Pet>();

    AsyncCallback callback = new AsyncCallback(CompletePets);
    Mock<IAsyncResult> mockAsyncResult = new Mock<IAsyncResult>();            
    Mock<IPetsController> petServiceTests = new Mock<IPetsController>();

    petServiceTests.Setup(async => async.EndGetAllPetsByAge(
                It.IsAny<IAsyncResult>())).Returns(this.lstPets);
    IPetsController value = petServiceTests.Object; 	// pass mock instance 
							// to interface
    expectedResult = value.EndGetAllPetsByAge(mockAsyncResult.Object);

    // using the callback to test that a method was indeed called.
    Assert.IsTrue(this.lstPets == expectedResult); 
}

模拟异步调用

下面,我正在执行一个异步测试,我正在模拟 WCF 服务中的“Begin”和“End”方法调用。我正在调用“ViewModel”类中的方法“ProcessAsync(int)”,该方法执行对服务的初始调用“Begin”。然后,我断言我期望完成事件传递回的值实际上已传递回。

/// <summary>
/// Tests the async completed.
/// </summary>
[Test]
public void TestAsyncCompleted()
{
    AsyncCallback callback = null;
    IAsyncResult ar = new Mock<IAsyncResult>().Object;
    var wcfService = new Mock<IPetsController>();

    // mock Begin
    wcfService.Setup(async => async.BeginGetAllPetsByAge(It.IsAny<int>(),
                It.IsAny<AsyncCallback>(),null))
                .Callback((int age, AsyncCallback cb, object state) => callback = cb)
                .Returns(ar);

    // mock End
    wcfService.Setup(async => async.EndGetAllPetsByAge(It.IsAny<IAsyncResult>()))   
                .Returns(this.lstPets);

    var sut = new ViewModel(wcfService.Object);
    sut.ProcessAsync(10);
    callback(ar);

    Assert.AreEqual(this.lstPets, sut.ListOfPets);          
}

在异步调用中模拟数据库调用

下面,我将一个模拟的数据库层对象注入到服务构造函数中,模拟一个特定的数据库调用方法“NumberOfKennelsInDatabase()”,该方法由服务方法“BeginServiceCallsDB”使用。下面的异步调用与上面使用的调用略有不同,因为我正在测试从异步调用返回的 IAsyncResult

 /// <summary>
        /// Tests the async method and mock DB call in service.
        /// </summary>
        [Test]
        public void TestAsyncMethodAndMockDBCallInService()
        {
            // mock the database call that will be performed inside the service method
            Mock<DatabaseLib.IDatabaseController> MockDB = 
                new Mock<DatabaseLib.IDatabaseController>();
            MockDB.Setup(meth => meth.NumberOfKennelsInDatabase()).Returns(2);

            AsyncCallback callback = new AsyncCallback(CompletePets);
            IAsyncResult expectedAasyncResult;
            IPetsController petServiceTests = new PetsController(MockDB.Object);
            expectedAasyncResult = petServiceTests.BeginServiceCallsDB(callback,
                new object());

            Assert.IsTrue(((Pets.CompletedAsyncResult<int>)
			(expectedAasyncResult)).Data == 4);

        }

有用链接

项目中使用的引用

MoqAndDotNet/TestsReferences.png

NUnit 结果

MoqAndDotNet/NunitResults.png

© . All rights reserved.