我目前在Visual Studio项目中遇到一个非常奇怪的单元测试问题。 我编写了一个LogManager,它接受各种参数,包括确定是否应该写入LogEntry的级别。现在我有2个单元测试来测试它们。两者都包含日志条目的另一个值。
这是我的第一堂课:
/// <summary>
/// Provides the <see cref="ArrangeActAssert" /> in which the tests will run.
/// </summary>
public abstract class Context : ArrangeActAssert
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="Context" />.
/// </summary>
protected Context()
{
MapperConfig.RegisterMappings();
var settingsRepository = new Repository<Setting>();
settingsRepository.Add(new Setting { Key = "LOGLEVEL", Value = "DEBUG" });
// Mock the IUnitOfWork.
var uow = new Mock<IUnitOfWork>();
uow.SetupGet(x => x.LogRepository).Returns(new Repository<Log>());
uow.SetupGet(x => x.SettingRepository).Returns(settingsRepository);
unitOfWork = uow.Object;
}
#endregion
#region Properties
/// <summary>
/// The <see cref="IUnitOfWork"/> which is used to access the data.
/// </summary>
protected IUnitOfWork unitOfWork;
#endregion Properties
#region Methods
#endregion Methods
}
/// <summary>
/// Test the bahviour when rwiting a new log.
/// </summary>
[TestClass]
public class when_writing_a_trace_log : Context
{
#region Context Members
/// <summary>
/// Write a log.
/// </summary>
protected override void Act()
{
var httpApplicationMock = new Mock<IHttpApplication>();
httpApplicationMock.SetupGet(x => x.IP).Returns("127.0.0.1");
httpApplicationMock.SetupGet(x => x.RequestIdentifier).Returns(Guid.NewGuid().ToString().ToUpper());
httpApplicationMock.SetupGet(x => x.UserIdentifier).Returns(Guid.NewGuid().ToString().ToUpper());
LogManager.Write(httpApplicationMock.Object, unitOfWork, LogLevel.Trace, "Unit test", "This message is being writted using the LogManager.");
}
#endregion
#region Methods
/// <summary>
/// Checks if the repository of the logs does contain an entry.
/// </summary>
[TestMethod]
public void then_the_repository_should_contain_another_log_entry()
{
Assert.AreEqual(0, unitOfWork.LogRepository.GetAll().Count(), "The repository containing the logs does either not contain an entry or has more than a single entry.");
}
#endregion
}
在上面的例子中,这个类只有一个方法可以测试,但实际上每个日志级别都有5.1个写入,可以是“跟踪”,“调试”和“#39” ;,信息&#39;,&#39;警告&#39;,&#39;错误&#39;或者&#39;严重&#39;。
本课程的目的如下:
现在,我有一个具有相同方法的类的副本,唯一的区别是上下文构造函数,其中包含一个LOGLEVEL&#39; DEBUG&#39;在设置存储库中。
当我现在运行所有单元测试时,一个测试失败(这对我来说没有意义,因为它之前正在工作,我还没有改变代码 - 除了添加单元测试的新类)。当我调试失败的单元测试时,一切都是正确的。
最后,这是受测试的课程:
public static class LogManager
{
#region Methods
/// <summary>
/// Writes a log message.
/// </summary>
/// <param name="application">The <see cref="IHttpApplication"/> which is needed to write a log entry.</param>
/// <param name="unitOfWork">The <see cref="IUnitOfWork" /> used to save the message.</param>
/// <param name="level">The <see cref="LogLevel" /> that the message should have.</param>
/// <param name="title">The tiel of that the message should have.</param>
/// <param name="message">The message to write.</param>
public static void Write(IHttpApplication application, IUnitOfWork unitOfWork, LogLevel level, string title, string message, params AdditionalProperty[] properties)
{
if (CanLogOnLevel(unitOfWork, level))
{
var entry = new LogViewModel
{
Level = (Data.Enumerations.LogLevel)level,
Client = application.IP,
UserIdentifier = application.UserIdentifier,
RequestIdentifier = application.RequestIdentifier,
Title = title,
Message = message,
AdditionalProperties = new AdditionalProperties() { Properties = properties.ToList() }
};
unitOfWork.LogRepository.Add(Mapper.Map<Log>(entry));
}
}
/// <summary>
/// Check if the log should be saved, depending on the <see cref="LogLevel" />.
/// </summary>
/// <param name="unitOfWork">The <see cref="IUnitOfWork" /> used to determine if the log should be written.</param>
/// <param name="level">The <see cref="LogLevel" /> of the log.</param>
/// <returns><see langword="true" /> when the log should be written, otherwise <see langword="false" />.</returns>
private static bool CanLogOnLevel(IUnitOfWork unitOfWork, LogLevel level)
{
LogLevel lowestLogLevel = SingletonInitializer<SettingsManager>.GetInstance(unitOfWork).LogLevel;
switch (lowestLogLevel)
{
case LogLevel.None:
return false;
case LogLevel.Trace:
return level == LogLevel.Trace || level == LogLevel.Debug || level == LogLevel.Information ||
level == LogLevel.Warning || level == LogLevel.Error || level == LogLevel.Critical;
case LogLevel.Debug:
return level == LogLevel.Debug || level == LogLevel.Information || level == LogLevel.Warning ||
level == LogLevel.Error || level == LogLevel.Critical;
case LogLevel.Information:
return level == LogLevel.Information || level == LogLevel.Warning || level == LogLevel.Error ||
level == LogLevel.Critical;
case LogLevel.Warning:
return level == LogLevel.Warning || level == LogLevel.Error || level == LogLevel.Critical;
case LogLevel.Error:
return level == LogLevel.Error || level == LogLevel.Critical;
case LogLevel.Critical:
return level == LogLevel.Critical;
default:
return false;
}
}
#endregion
}
有人有线索吗?
这个单元测试在这个单元测试失败时通过:
/// <summary>
/// Provides the <see cref="ArrangeActAssert" /> in which the tests will run.
/// </summary>
public abstract class Context : ArrangeActAssert
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="Context" />.
/// </summary>
protected Context()
{
MapperConfig.RegisterMappings();
var settingsRepository = new Repository<Setting>();
settingsRepository.Add(new Setting { Key = "LOGLEVEL", Value = "DEBUG" });
// Mock the IUnitOfWork.
var uow = new Mock<IUnitOfWork>();
uow.SetupGet(x => x.LogRepository).Returns(new Repository<Log>());
uow.SetupGet(x => x.SettingRepository).Returns(settingsRepository);
unitOfWork = uow.Object;
}
#endregion
#region Properties
/// <summary>
/// The <see cref="IUnitOfWork"/> which is used to access the data.
/// </summary>
protected IUnitOfWork unitOfWork;
#endregion Properties
#region Methods
#endregion Methods
}
/// <summary>
/// Test the bahviour when rwiting a new log.
/// </summary>
[TestClass]
public class when_writing_a_trace_log : Context
{
#region Context Members
/// <summary>
/// Write a log.
/// </summary>
protected override void Act()
{
var httpApplicationMock = new Mock<IHttpApplication>();
httpApplicationMock.SetupGet(x => x.IP).Returns("127.0.0.1");
httpApplicationMock.SetupGet(x => x.RequestIdentifier).Returns(Guid.NewGuid().ToString().ToUpper());
httpApplicationMock.SetupGet(x => x.UserIdentifier).Returns(Guid.NewGuid().ToString().ToUpper());
LogManager.Write(httpApplicationMock.Object, unitOfWork, LogLevel.Trace, "Unit test", "This message is being writted using the LogManager.");
}
#endregion
#region Methods
/// <summary>
/// Checks if the repository of the logs does contain an entry.
/// </summary>
[TestMethod]
public void then_the_repository_should_contain_another_log_entry()
{
Assert.AreEqual(0, unitOfWork.LogRepository.GetAll().Count(), "The repository containing the logs does either not contain an entry or has more than a single entry.");
}
#endregion
}
这里是ArrangeAct类:
/// <summary>
/// A base class for written in the BDD (behaviour driven development) that provide standard
/// methods to set up test actions and the "when" statements. "Then" is encapsulated by the
/// testmethods themselves.
/// </summary>
public abstract class ArrangeActAssert
{
#region Methods
/// <summary>
/// When overridden in a derived class, this method is used to perform interactions against
/// the system under test.
/// </summary>
/// <remarks>
/// This method is called automatticly after the <see cref="Arrange" /> method and before
/// each test method runs.
/// </remarks>
protected virtual void Act()
{
}
/// <summary>
/// When overridden in a derived class, this method is used to set up the current state of
/// the specs context.
/// </summary>
/// <remarks>
/// This method is called automatticly before every test, before the <see cref="Act" /> method.
/// </remarks>
protected virtual void Arrange()
{
}
/// <summary>
/// When overridden in a derived class, this method is used to reset the state of the system
/// after a test method has completed.
/// </summary>
/// <remarks>
/// This method is called automatticly after each testmethod has run.
/// </remarks>
protected virtual void Teardown()
{
}
#endregion Methods
#region MSTest integration
[TestInitialize]
public void MainSetup()
{
Arrange();
Act();
}
[TestCleanup]
public void MainTeardown()
{
Teardown();
}
#endregion MSTest integration
}
但是当在一次测试中运行相同的测试与其他测试一起运行时,测试失败。
答案 0 :(得分:3)
你在构造函数中做的事情应该在test initializer中完成:
e.g。而不是:
protected Context()
{
MapperConfig.RegisterMappings();
var settingsRepository = new Repository<Setting>();
settingsRepository.Add(new Setting { Key = "LOGLEVEL", Value = "DEBUG" });
// Mock the IUnitOfWork.
var uow = new Mock<IUnitOfWork>();
uow.SetupGet(x => x.LogRepository).Returns(new Repository<Log>());
uow.SetupGet(x => x.SettingRepository).Returns(settingsRepository);
unitOfWork = uow.Object;
}
这样做:
protected Context()
{
MapperConfig.RegisterMappings();
}
[TestInitialize]
protected void Setup()
{
var settingsRepository = new Repository<Setting>();
settingsRepository.Add(new Setting { Key = "LOGLEVEL", Value = "DEBUG" });
// Mock the IUnitOfWork.
var uow = new Mock<IUnitOfWork>();
uow.SetupGet(x => x.LogRepository).Returns(new Repository<Log>());
uow.SetupGet(x => x.SettingRepository).Returns(settingsRepository);
unitOfWork = uow.Object;
}
因此,在每次测试之前,你都会得到一个干净的模拟工作单元。
测试运行如下:
constructor
[ClassInitialize] methods.
for each [TestMethod]
[TestInitlize] methods.
[TestMethod]
[TestCleanup] methods.
[ClassCleanup] methods.
TestMethods的顺序永远不应该重要,即你永远不要依赖订单。在您的情况下,如果在检查它为空的测试之前运行向该工作单元添加日志条目的测试,则该测试将失败。解决方案是始终以干净的工作单位开始。
错误测试的简单示例:
[TestClass]
public class Test
{
private List<int> list;
public Test()
{
list = new List<int>();
}
[TestMethod]
public void can_add_to_list()
{
list.Add(10);
Assert.areEqual(1, list.Count);
}
[TestMethod]
public void can_add_two_to_list()
{
list.Add(10);
list.Add(20);
Assert.areEqual(2, list.Count);
}
}
这些测试将始终可以自行运行,但是当它们一起运行时,其中一个将失败,因为在每次测试之前不会创建新的列表。