简体   繁体   English

C#单元测试,它们如何表现

[英]C# Unit Tests, how do they behave

I'm currently having a very strange issue with Unit Tests in my Visual Studio project. 我的Visual Studio项目目前在单元测试中遇到一个非常奇怪的问题。 I have written a LogManager that takes various parameters, including a level to determine if the LogEntry should be written. 我编写了一个LogManager,它采用各种参数,包括确定是否应编写LogEntry的级别。 Now I have 2 unit tests that tests them. 现在,我有2个测试它们的单元测试。 Both including another value of the log entries. 两者都包括日志条目的另一个值。

Here's my first class: 这是我的第一堂课:

/// <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
}

In the example above, this class has a single method to test, but in fact there are 5. 1 for writing at each log level which can be 'Trace', 'Debug', Information', 'Warning', 'Error' or 'Critical'. 在上面的示例中,此类有一个测试方法,但实际上在每个日志级别有5. 1个用于编写的代码,可以是'Trace','Debug',Information','Warning','Error'或'危急'。

The purpose of this class is the following: 此类的目的如下:

  • Is a log entry saved at a particular level when the level in the database is set to 'TRACE'. 当数据库中的级别设置为“ TRACE”时,是否将日志条目保存在特定级别。

Now, I have a copy of this class that has the same methods, to only difference is the context constructor which holds a LOGLEVEL of 'DEBUG' in the settings repository. 现在,我有一个具有相同方法的此类的副本,唯一的不同是上下文构造函数在设置存储库中持有LOGLEVEL'DEBUG'。

When I run all the unit tests right now, one test is failing (which makes no sense to me because it was working before and I haven't changed the code - apart from adding new classes with unit tests). 当我现在运行所有的单元测试时,一个测试失败了(这对我来说没有意义,因为它以前可以工作,并且我没有更改代码-除了使用单元测试添加新类之外)。 When I debug the failed unit test, everything is correct. 当我调试失败的单元测试时,一切都正确。

And finally, here's the class under test: 最后,这是要测试的课程:

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
}

Someone has a clue? 有人有线索吗?

This unit test is passing when this unit test is runt: 单元测试失败时,该单元测试通过:

/// <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
}

Here's the ArrangeAct class: 这是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
}

But when that same test in run in one single test run with other tests, the test is failing. 但是,当同一测试与其他测试一起运行时,该测试将失败。

You're doing stuff in a constructor that should be done in a test initializer : 您正在使用应在测试初始化​​程序中完成的构造函数中进行操作:

eg instead of: 例如代替:

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;
}

Do this: 做这个:

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;
}

So that before each test you get a clean mock unit of work. 这样,在每次测试之前,您都会获得一个干净的模拟工作单元。

A test run looks like this: 测试运行如下所示:

constructor
[ClassInitialize] methods.
for each [TestMethod]
  [TestInitlize] methods.
  [TestMethod]
  [TestCleanup] methods.
[ClassCleanup] methods.

The order of the TestMethods should never matter, ie you should never rely on the order. TestMethods的顺序应该无关紧要,即,您永远不应依赖该顺序。 In your case if a test that adds a log entry to that unit of work runs before the test that checks that it's empty, then that test is going to fail. 在您的情况下,如果在向该工作单元添加日志条目的测试之前执行了检查以检查是否为空的测试,则该测试将失败。 The solution is to always start with a clean unit of work. 解决方案是始终从干净的工作单元开始。

Simple example of a bad test: 不良测试的简单示例:

[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);
   }
}

These tests will always work on their own, but when run together one of them will fail because the list is not created a fresh before each test. 这些测试将始终独立运行,但是当它们一起运行时,其中一个将失败,因为在每次测试之前,列表都不是全新创建的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM