Complexity
Complexity

Reputation: 5820

C# Unit Tests, how do they behave

I'm currently having a very strange issue with Unit Tests in my Visual Studio project. I have written a LogManager that takes various parameters, including a level to determine if the LogEntry should be written. Now I have 2 unit tests that tests them. 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'.

The purpose of this class is the following:

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.

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:

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

Upvotes: 0

Views: 991

Answers (1)

weston
weston

Reputation: 54781

You're doing stuff in a constructor that should be done in a test initializer:

e.g. 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, i.e. you should never rely on the order. 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.

Upvotes: 3

Related Questions