单元测试在 运行 上失败,但在 运行 单身时通过
Unit tests fail on Run All but pass when are run single
我有这个测试 class 并且当我 运行 这些测试一次一个通过时,但是当我尝试 运行 来自这个 class 的所有测试时,第一个测试总是通过,其他测试通常失败(有时随机其中一个通过),当我 运行 我项目中的所有测试时,所有这些测试都失败了。
我使用 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());
}
}
}
你知道可能是什么原因吗?
如您所见,我将所有可重复代码移至这些方法以避免依赖关系,但它不起作用。
正如您提到的,由于每个测试都在创建自己的数据并且没有任何共享对象,因此导致这些失败的可能原因有几个:
选项A
您的被测系统(即 LoginPageViewModel)正在以线程安全的方式重用其依赖项。
如果是这种情况,那么 LoginPageViewModel 将使用在给定线程上 运行 的第一个测试中创建的模拟接口进行实例化。然后它将为该线程上的任何其他测试 运行 重新使用这些依赖项。这将导致该线程上的第一个测试通过(因此当您 运行 每个测试单独进行时,它们都会通过)。但是,该线程上的任何后续测试 运行 都将失败,因为即使您为每个测试传递了新的模拟接口,该线程上第一个测试的模拟接口也会被重新使用。
这个问题的解决方案是:
- 删除代码中的任何锁定或 Singelton 实现,而是使用 IoC 容器来定义应用程序中对象的生活方式。然后您可以控制测试中使用的对象的生命周期。
- 使用 NUnit 中的 RequiresThread attribute 确保每个测试 运行 在它自己的线程上,消除实例重用。
选项 B(进一步查看您的代码后似乎是这种情况)
您的被测系统正在调用一些异步代码而不等待结果。这将导致异步代码继续 运行 而控制权返回给调用者(在本例中为单元测试)。如果测试中的断言在异步代码完成之前执行,则这可能会导致竞争条件。当代码由于某些负载而执行速度较慢时,这些竞争条件更有可能发生,例如 运行 同时进行多个测试。
您正在测试的代码中似乎就是这种情况。 LoginPageViewModel constructs a RelayParameterizedCommand 的构造函数,传入一个异步委托。但是,您的单元测试然后调用 RelayParameterizedCommand 来执行传入的委托,而不等待结果。
解决方案是:
- 将传递给 RelayParameterizedCommand 的委托从
Action<object>
更新为 Func<object, Task>
。那么你也可以
- 使 RelayParameterizedCommand 上的 Execute 方法异步。然后 make your unit tests aysnc 并使用 await 调用被测系统中的方法。或者,
- 保持 RelayParameterizedCommand 上的 Execute 方法同步,但仍然从委托的任务中获取结果:
_action(parameter).GetAwaiter().GetResult();
我有这个测试 class 并且当我 运行 这些测试一次一个通过时,但是当我尝试 运行 来自这个 class 的所有测试时,第一个测试总是通过,其他测试通常失败(有时随机其中一个通过),当我 运行 我项目中的所有测试时,所有这些测试都失败了。
我使用 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());
}
}
}
你知道可能是什么原因吗? 如您所见,我将所有可重复代码移至这些方法以避免依赖关系,但它不起作用。
正如您提到的,由于每个测试都在创建自己的数据并且没有任何共享对象,因此导致这些失败的可能原因有几个:
选项A
您的被测系统(即 LoginPageViewModel)正在以线程安全的方式重用其依赖项。
如果是这种情况,那么 LoginPageViewModel 将使用在给定线程上 运行 的第一个测试中创建的模拟接口进行实例化。然后它将为该线程上的任何其他测试 运行 重新使用这些依赖项。这将导致该线程上的第一个测试通过(因此当您 运行 每个测试单独进行时,它们都会通过)。但是,该线程上的任何后续测试 运行 都将失败,因为即使您为每个测试传递了新的模拟接口,该线程上第一个测试的模拟接口也会被重新使用。
这个问题的解决方案是:
- 删除代码中的任何锁定或 Singelton 实现,而是使用 IoC 容器来定义应用程序中对象的生活方式。然后您可以控制测试中使用的对象的生命周期。
- 使用 NUnit 中的 RequiresThread attribute 确保每个测试 运行 在它自己的线程上,消除实例重用。
选项 B(进一步查看您的代码后似乎是这种情况)
您的被测系统正在调用一些异步代码而不等待结果。这将导致异步代码继续 运行 而控制权返回给调用者(在本例中为单元测试)。如果测试中的断言在异步代码完成之前执行,则这可能会导致竞争条件。当代码由于某些负载而执行速度较慢时,这些竞争条件更有可能发生,例如 运行 同时进行多个测试。
您正在测试的代码中似乎就是这种情况。 LoginPageViewModel constructs a RelayParameterizedCommand 的构造函数,传入一个异步委托。但是,您的单元测试然后调用 RelayParameterizedCommand 来执行传入的委托,而不等待结果。
解决方案是:
- 将传递给 RelayParameterizedCommand 的委托从
Action<object>
更新为Func<object, Task>
。那么你也可以- 使 RelayParameterizedCommand 上的 Execute 方法异步。然后 make your unit tests aysnc 并使用 await 调用被测系统中的方法。或者,
- 保持 RelayParameterizedCommand 上的 Execute 方法同步,但仍然从委托的任务中获取结果:
_action(parameter).GetAwaiter().GetResult();