Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test Environment.Exit() in C#

Is there in C# some kind of equivalent of ExpectedSystemExit in Java? I have an exit in my code and would really like to be able to test it. The only thing I found in C# is a not really nice workaround.

Example Code

public void CheckRights()
{
    if(!service.UserHasRights())
    {
         Environment.Exit(1);
    }
}

Test Code

[TestMethod]
public void TestCheckRightsWithoutRights()
{
    MyService service = ...
    service.UserHasRights().Returns(false);

    ???
}

I am using the VS framework for testing (+ NSubstitute for mocking) but it is not a problem to switch to nunit or whatever for this test.

like image 500
Antiohia Avatar asked May 20 '16 07:05

Antiohia


2 Answers

You should use dependency injection to supply to the class being tested an interface that provides an environmental exit.

For example:

public interface IEnvironment
{
    void Exit(int code);
}

Let's also assume that you have an interface for calling UserHasRights():

public interface IRightsService
{
    bool UserHasRights();
}

Now suppose your class to be tested looks like this:

public sealed class RightsChecker
{
    readonly IRightsService service;
    readonly IEnvironment environment;

    public RightsChecker(IRightsService service, IEnvironment environment)
    {
        this.service     = service;
        this.environment = environment;
    }

    public void CheckRights()
    {
        if (!service.UserHasRights())
        {
            environment.Exit(1);
        }
    }
}

Now you can use a mocking framework to check that IEnvironment .Exit() is called under the right conditions. For example, using Moq it might look a bit like this:

[TestMethod]
public static void CheckRights_exits_program_when_user_has_no_rights()
{
    var rightsService = new Mock<IRightsService>();
    rightsService.Setup(foo => foo.UserHasRights()).Returns(false);

    var enviromnent = new Mock<IEnvironment>();

    var rightsChecker = new RightsChecker(rightsService.Object, enviromnent.Object);

    rightsChecker.CheckRights();

    enviromnent.Verify(foo => foo.Exit(1));
}

Ambient contexts and cross-cutting concerns

A method such as Environment.Exit() could be considered to be a cross-cutting concern, and you might well want to avoid passing around an interface for it because you can end up with an explosion of additional constructor parameters. (Note: The canonical example of a cross cutting concern is DateTime.Now.)

To address this issue, you can introduce an "Ambient context" - a pattern which allows you to use a static method while still retaining the ability to unit test calls to it. Of course, such things should be used sparingly and only for true cross-cutting concerns.

For example, you could introduce an ambient context for Environment like so:

public abstract class EnvironmentControl
{
    public static EnvironmentControl Current
    {
        get
        {
            return _current;
        }

        set
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));

            _current = value;
        }
    }

    public abstract void Exit(int value);

    public static void ResetToDefault()
    {
        _current = DefaultEnvironmentControl.Instance;
    }

    static EnvironmentControl _current = DefaultEnvironmentControl.Instance;
}

public class DefaultEnvironmentControl : EnvironmentControl
{
    public override void Exit(int value)
    {
        Environment.Exit(value);
    }

    public static DefaultEnvironmentControl Instance => _instance.Value;

    static readonly Lazy<DefaultEnvironmentControl> _instance = new Lazy<DefaultEnvironmentControl>(() => new DefaultEnvironmentControl());
}

Normal code just calls EnvironmentControl.Current.Exit(). With this change, the IEnvironment parameter disappears from the RightsChecker class:

public sealed class RightsChecker
{
    readonly IRightsService service;

    public RightsChecker(IRightsService service)
    {
        this.service = service;
    }

    public void CheckRights()
    {
        if (!service.UserHasRights())
        {
            EnvironmentControl.Current.Exit(1);
        }
    }
}

But we still retain the ability to unit-test that it has been called:

public static void CheckRights_exits_program_when_user_has_no_rights()
{
    var rightsService = new Mock<IRightsService>();
    rightsService.Setup(foo => foo.UserHasRights()).Returns(false);

    var enviromnent = new Mock<EnvironmentControl>();
    EnvironmentControl.Current = enviromnent.Object;

    try
    {
        var rightsChecker = new RightsChecker(rightsService.Object);
        rightsChecker.CheckRights();
        enviromnent.Verify(foo => foo.Exit(1));
    }

    finally
    {
        EnvironmentControl.ResetToDefault();
    }
}

For more information about ambient contexts, see here.

like image 68
Matthew Watson Avatar answered Oct 13 '22 00:10

Matthew Watson


I ended up creating a new method which I can then mock in my tests.

Code

public void CheckRights()
{
    if(!service.UserHasRights())
    {
         Environment.Exit(1);
    }
}

internal virtual void Exit() 
{
    Environment.Exit(1);
}

Unit test

[TestMethod]
public void TestCheckRightsWithoutRights()
{
    MyService service = ...
    service.When(svc => svc.Exit()).DoNotCallBase();
    ...
    service.CheckRights();
    service.Received(1).Exit();
}
like image 41
Antiohia Avatar answered Oct 13 '22 00:10

Antiohia