user4593252
user4593252

Reputation: 3506

How to call async from [TestMethod]?

I have a rather complicated method in a WebAPI MVC project. It does a number of things including hitting a remote server for user authentication. Depending on the results of this, it returns either a redirect (a page), a string error message, or an object representing all of the authentication monkey-business.

It works great when called from a browser. It even works just fine in debug. I can even write a redirect to it and call it "manually", passing in whatever params it needs.

The issue I'm running into is calling it from a test project VS made when I created the WebAPI project. I suspect that it's because because of all the async and await that gets thrown about. When it goes into it, eventually it comes back with a "Object not set to instance of an object" error.

Since it works in any other context, I assume that it's because it's inside of a test project and needs to be awaited. Can anyone give me any kind of advice on this?

Edit: to be very specific, it's failing on the second line of code here:

BoxConfig boxConfig = new BoxConfig(ClientID, ClientSecret, enterpriseID, prvt, JWTPublicKeyPass, JWTPublicKeyID);
BoxJWTAuth boxJWT = new BoxJWTAuth(boxConfig); //This is a JSON web token and is needed to authorize the enterprise level app user.

Code context:

This is leveraging the Box.com API. The BoxJWT call creates a JSON Web Token. I don't know where in the process it is failing because when I trace through it, it can't show me the code for things like PEMReader.cs, etc (which have to do with crypto, bouncy castle). But very specifically, the error message detail says the source is Box.V2.JWTAuth.PEMPasswordFinder.GetPassword()

Upvotes: 17

Views: 14463

Answers (4)

user4593252
user4593252

Reputation: 3506

In this very specific instance, information is being pulled from the web.config file when it is run.

When the Test project attempts to run, the context of "web.config" changes so that any custom information in the tested project's AppSettings, must be in the Test project's App.Config file as well. This behavior is consistent with security practices for connection strings and is therefore not surprising.

Adding this information to the Test Project's App.Config file has resolved the issue.

Upvotes: 0

David Pine
David Pine

Reputation: 24525

This is a great example use-case for the Moq framework. For example when you call attention to making a remote call, that service should be mocked. Ideally you should try to mock out the different variable aspects such that you can easily and consistently provide a known set of inputs to validate an expected set of outputs. Since you have not provided any source code, I'll make some assumptions -- share some sample code and corresponding unit tests.

public class LoginResult
{
    public string RedirectUrl { get; set; }
    public string FailureMsg { get; set; }
    public MonkeyBusiness AuthToken { get; set; }
}

[
    HttpPost,
    AllowAnonymous,
    Route("api/[controller]/login")
]
public async Task<LoginResult> Login(
    [FromBody] CredentialsModel credentials,
    [FromServices] ILogger logger,
    [FromServices] IAuthenticationModule authentication,
    [FromServices] IAuthClaimsPrincipalBuiler claimsPrincipalBuilder)
{
    try
    {
        var result = 
            await authentication.ExternalAuthenticateAsync(credentials);

        if (result.IsAuthenticated)
        {
            var principal = 
                await claimsPrincipalBuilder.BuildAsync(credentials);
            await HttpContext.Authentication.SignInAsync("Scheme", principal);
            return new LoginResult 
            {
                AuthToken = 
                    "Too much source to make up just for stackoverflow, " +
                    "but I hope you now get the point..."
            };
        }

        return new LoginResult { FailureMsg = "Invalid credentials." };
    }
    catch (Exception ex)
    {
        logger.LogError(ex.Message, ex);
        return new LoginResult { FailureMsg = ex.Message };
    }
}

Then you could have unit tests similar to this, in which you mock out specific implementations of interfaces. In this case you'd mock the IAuthenticationModule and have several tests ensuring that regardless of however the remote server returns your Login logic handles it as desired/expected. Mocking async stuff is very easy to do.

using xUnit;

[Fact]
public async Task LoginThrowsAsExpectedTest()
{   
    // Arrange     
    var controller = new LoginController();
    var loggerMock = new Mock<ILogger>();
    var authModuleMock = new Mock<IAuthenticationModule>();

    var expectedMessage = "I knew it!";       

    // Setup async methods! 
    authModuleMock.Setup(am => am.ExternalAuthenticateAsync(It.IsAny<CredentialsModel>())
                  .ThrowsAsync(new Exception(expectedMessage));

    // Act
    var result = 
        await controller.Login(
            new CredentialsModel(),
            loggerMock.Object,
            authModuleMock.Object,
            null);


    // Assert
    Assert.Equal(expectedMessage, result.FailureMsg);
}

Or one that you'd expect to correctly authenticate.

using xUnit;

[Fact]
public async Task LoginHandlesIsNotAuthenticatedTest()
{   
    // Arrange     
    var controller = new LoginController();
    var loggerMock = new Mock<ILogger>();
    var authModuleMock = new Mock<IAuthenticationModule>();        

    var expectedMessage = "Invalid credentials."; 

    // Setup async methods! 
    authModuleMock.Setup(am => am.ExternalAuthenticateAsync(It.IsAny<CredentialsModel>())
                  .ReturnsAsync(new AuthResult { IsAuthenticated = false });        

    // Act
    var result = 
        await controller.Login(
            new CredentialsModel(),
            loggerMock.Object,
            authModuleMock.Object,
            null);


    // Assert
    Assert.Equal(expectedMessage, result.FailureMsg);
}

The point is that you can still write async and await unit tests using Moq fairly easily...

Upvotes: 1

MichaelChan
MichaelChan

Reputation: 1836

You can use GetAwaiter and GetResult where Execute is the asynchronous method you want to test.

var mockParam = new Mock<IParam>();
var sut = new MyClass(mockParam);
var result = sut.Execute().GetAwaiter().GetResult();

You can find more information here. https://msdn.microsoft.com/en-us/magazine/dn818493.aspx?f=255&MSPPError=-2147217396. It briefly mentions other ways you could test it but this method is the one that will give you the most accurate result.

Upvotes: -1

davidallyoung
davidallyoung

Reputation: 1332

Whenever I create a test method that is going to test async code I make sure the signature of the test method is Async and returns Task. This allows you to avoid the dreaded deadlock that comes from async void. Now you can await inside of the test method.

More info: https://msdn.microsoft.com/en-us/magazine/dn818493.aspx

Example:

[TestMethod]
public async Task myTestMethod()
{
  await MyMethodToTest();
}

Upvotes: 26

Related Questions