单元测试在“全部运行”时失败,但在单次运行时通过

时间:2018-12-30 20:51:15

标签: c# unit-testing nunit moq

我有这个测试类,当我一次运行一个测试时,它们通过了,但是当我尝试运行该类中的所有测试时,总是会通过第一个测试,而其他测试通常会失败(有时会其中一个通过),并且当我运行项目中的所有测试时,所有这些测试都会失败。

我使用NUnit和Moq框架。

代码如下:

using System.Security;
using DebtDiary.Core;
using DebtDiary.DataProvider;
using Moq;
using NUnit.Framework;

namespace DebtDiary.Tests.ViewModels
{
    [TestFixture]
    public class LoginPageViewModelTests
    {
        [Test]
        public void TestLoginCommandCallsLoginUserInClientDataStoreWhenDataIsValid()
        {
            Mock<IApplicationViewModel> stubApplicationVM = new Mock<IApplicationViewModel>();
            Mock<IDiaryPageViewModel> stubDiaryPageVM = new Mock<IDiaryPageViewModel>();
            Mock<IDialogFacade> stubDialogFacadeVM = new Mock<IDialogFacade>();
            Mock<IClientDataStore> mockClientDataStore = new Mock<IClientDataStore>();
            Mock<IDataAccess> stubDataAccess = new Mock<IDataAccess>();
            var loginPageVM = new LoginPageViewModel(stubApplicationVM.Object, stubDiaryPageVM.Object, stubDialogFacadeVM.Object, mockClientDataStore.Object, stubDataAccess.Object);
            loginPageVM.Username = "test";
            Mock<IHavePassword> stubPassword = new Mock<IHavePassword>();
            SecureString ss = new SecureString();
            ss.AppendChar('t');
            stubPassword.Setup(x => x.Password).Returns(ss);
            User user = new User();
            stubDataAccess.Setup(x => x.UserExist(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
            stubDataAccess.Setup(x => x.TryGetUser(It.IsAny<string>(), It.IsAny<string>(), out user)).Returns(true);

            loginPageVM.LoginCommand.Execute(stubPassword.Object);

            mockClientDataStore.Verify(x => x.LoginUser(It.IsAny<User>()), Times.Once());
        }

        [Test]
        public void TestLoginCommandUpdatesDebtorsListInDiaryPageViewModelWhenDataIsValid()
        {
            Mock<IApplicationViewModel> stubApplicationVM = new Mock<IApplicationViewModel>();
            Mock<IDiaryPageViewModel> mockDiaryPageVM = new Mock<IDiaryPageViewModel>();
            Mock<IDialogFacade> stubDialogFacadeVM = new Mock<IDialogFacade>();
            Mock<IClientDataStore> stubClientDataStore = new Mock<IClientDataStore>();
            Mock<IDataAccess> stubDataAccess = new Mock<IDataAccess>();
            var loginPageVM = new LoginPageViewModel(stubApplicationVM.Object, mockDiaryPageVM.Object, stubDialogFacadeVM.Object, stubClientDataStore.Object, stubDataAccess.Object);
            loginPageVM.Username = "test";
            Mock<IHavePassword> stubPassword = new Mock<IHavePassword>();
            SecureString ss = new SecureString();
            ss.AppendChar('t');
            stubPassword.Setup(x => x.Password).Returns(ss);
            User user = new User();
            stubDataAccess.Setup(x => x.UserExist(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
            stubDataAccess.Setup(x => x.TryGetUser(It.IsAny<string>(), It.IsAny<string>(), out user)).Returns(true);

            loginPageVM.LoginCommand.Execute(stubPassword.Object);

            mockDiaryPageVM.Verify(x => x.UpdateDebtorsList(), Times.Once());
        }

        [Test]
        public void TestLoginCommandUpdatesUsersDataInDiaryPageViewModelWhenDataIsValid()
        {
            Mock<IApplicationViewModel> stubApplicationVM = new Mock<IApplicationViewModel>();
            Mock<IDiaryPageViewModel> mockDiaryPageVM = new Mock<IDiaryPageViewModel>();
            Mock<IDialogFacade> stubDialogFacadeVM = new Mock<IDialogFacade>();
            Mock<IClientDataStore> stubClientDataStore = new Mock<IClientDataStore>();
            Mock<IDataAccess> stubDataAccess = new Mock<IDataAccess>();
            var loginPageVM = new LoginPageViewModel(stubApplicationVM.Object, mockDiaryPageVM.Object, stubDialogFacadeVM.Object, stubClientDataStore.Object, stubDataAccess.Object);
            loginPageVM.Username = "test";
            Mock<IHavePassword> stubPassword = new Mock<IHavePassword>();
            SecureString ss = new SecureString();
            ss.AppendChar('t');
            stubPassword.Setup(x => x.Password).Returns(ss);
            User user = new User();
            stubDataAccess.Setup(x => x.UserExist(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
            stubDataAccess.Setup(x => x.TryGetUser(It.IsAny<string>(), It.IsAny<string>(), out user)).Returns(true);

            loginPageVM.LoginCommand.Execute(stubPassword.Object);

            mockDiaryPageVM.Verify(x => x.UpdateUsersData(), Times.Once());
        }

        [Test]
        public void TestLoginCommandResetsCurrentSubpageInApplicationViewModelWhenDataIsValid()
        {
            Mock<IApplicationViewModel> mockApplicationVM = new Mock<IApplicationViewModel>();
            Mock<IDiaryPageViewModel> stubDiaryPageVM = new Mock<IDiaryPageViewModel>();
            Mock<IDialogFacade> stubDialogFacadeVM = new Mock<IDialogFacade>();
            Mock<IClientDataStore> stubClientDataStore = new Mock<IClientDataStore>();
            Mock<IDataAccess> stubDataAccess = new Mock<IDataAccess>();
            var loginPageVM = new LoginPageViewModel(mockApplicationVM.Object, stubDiaryPageVM.Object, stubDialogFacadeVM.Object, stubClientDataStore.Object, stubDataAccess.Object);
            loginPageVM.Username = "test";
            Mock<IHavePassword> stubPassword = new Mock<IHavePassword>();
            SecureString ss = new SecureString();
            ss.AppendChar('t');
            stubPassword.Setup(x => x.Password).Returns(ss);
            User user = new User();
            stubDataAccess.Setup(x => x.UserExist(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
            stubDataAccess.Setup(x => x.TryGetUser(It.IsAny<string>(), It.IsAny<string>(), out user)).Returns(true);

            loginPageVM.LoginCommand.Execute(stubPassword.Object);

            mockApplicationVM.Verify(x => x.ResetCurrentSubpage(), Times.Once());
        }
    }
}

您知道这可能是什么原因吗? 如您所见,我将所有可重复的代码都移到了这些方法上,以避免依赖,并且它不起作用。

1 个答案:

答案 0 :(得分:0)

由于您已经提到,每个测试都在创建自己的数据,并且没有任何共享对象,所以导致这些失败的原因可能有以下几种:

选项A

您的被测系统(即LoginPageViewModel)正在以线程安全的方式重用其依赖项。

如果是这种情况,则使用在给定线程上运行的第一个测试中创建的模拟接口实例化LoginPageViewModel。然后,它将在那些线程上进行的任何其他测试中重新使用这些依赖项。这将导致该线程上的第一个测试通过(因此,当您单独运行每个测试时,它们都将通过)。但是,在该线程上运行的任何后续测试都将失败,因为即使您为每个测试传递了新的模拟接口,该线程上第一个测试的模拟接口也将被重用。

解决此问题的方法是:

  1. 删除代码中的所有锁定或Singelton实现,而使用IoC容器定义对象在应用程序中的使用期限。然后,您可以控制测试中使用的对象的生活方式。
  2. 在NUnit中使用RequiresThread attribute,以确保每个测试都在其自己的线程上运行,从而消除了实例的重用。

选项B (在进一步查看您的代码后,似乎就是这种情况)

您的被测系统正在调用一些异步代码,而无需等待结果。这将导致异步代码继续运行,同时将控件返回给调用者(在本例中为单元测试)。如果在异步代码完成之前执行测试中的断言,则可能会导致争用情况。而且,由于某些负载(例如一次运行多个测试),导致代码执行速度较慢时,更容易出现这些竞争条件。

您正在测试的代码似乎就是这种情况。 LoginPageViewModel的构造函数构造一个RelayParameterizedCommand,并传入异步委托。但是,您的单元测试然后调用RelayParameterizedCommand来执行传入的委托,而无需等待结果。

解决方案是:

  1. 将传递给RelayParameterizedCommand的委托从Action<object>更新为Func<object, Task>。那你可以
    1. 对RelayParameterizedCommand异步执行Execute方法。然后make your unit tests aysnc并在等待中调用被测系统中的方法。或者,
    2. 使Execute方法与RelayParameterizedCommand保持同步,但仍从委托任务_action(parameter).GetAwaiter().GetResult();
    3. 中获取结果。