1

This is my test method in XUnit.

    [Fact]
    public async Task AddCampaign_ReturnBadRequestWhenDateIsInvalid()
    {
        var client = _factory.CreateClient();
        string title = string.Format("Test Add Campaign {0}", Guid.NewGuid());
        var campaignAddDto = new CampaignDTORequest
        {
            Title = title
        };
        var encodedContent = new StringContent(JsonConvert.SerializeObject(campaignAddDto), Encoding.UTF8, "application/json");

        var response = await client.PostAsync("/api/Campaign/add", encodedContent);
        var responseString = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<CampaignDTOResponse>(responseString);

        Assert.False(response.IsSuccessStatusCode);
        Assert.ThrowsAsync<ArgumentNullException>(()=> client.PostAsync("/api/Campaign/add", encodedContent));
    }

The first assert is works. I am stuck with second assert. How do I assert both exception type (ArgumentNullException) and its exception message?

This is the service method

    public async Task<Campaign> AddCampaignAsync(Campaign campaign)
    {            
        if (campaign.StartDate.Equals(DateTime.MinValue)) {
            throw new ArgumentNullException("Start Date cannot be null or empty.");
        }
        
        await _context.Campaigns.AddAsync(campaign);
        await _context.SaveChangesAsync();
        return campaign;
    }

Updated after the clue from Lei Yang.

var exceptionDetails = Assert.ThrowsAsync<ArgumentNullException>(() => client.PostAsync("/api/Campaign/add", encodedContent));
Assert.Equal("Start Date cannot be null or empty.", exceptionDetails.Result.Message);

But still doesn't work.

System.AggregateException : One or more errors occurred. (Assert.Throws() Failure Expected: typeof(System.ArgumentNullException) Actual: (No exception was thrown))

Tried the solution from Dai but still got error.

Assert.Throws() Failure
Expected: typeof(System.ArgumentNullException)
Actual:   (No exception was thrown)

This is my API method.

 public async Task<ActionResult<CampaignDTOResponse>> AddCampaign([FromBody] CampaignDTORequest newCampaign)
    {
        try
        {
            var campaign = _mapper.Map<Campaign>(newCampaign);
            campaign = await _campaignService.AddCampaignAsync(campaign);
            var campaignDtoResponse = _mapper.Map<CampaignDTOResponse>(campaign);
            return CreatedAtAction(nameof(GetCampaignById), new { id = campaignDtoResponse.Id }, campaignDtoResponse);
        }
        catch (Exception ex)
        {
            _logger.LogError(0, ex, ex.Message);
            return Problem(ex.Message);
        }
    }

Updated: I move the checking from service to api.

if (newCampaign.StartDate.Equals(DateTime.MinValue))
{
    return BadRequest("Start Date cannot be null or empty.");
}

and I assert them like below.

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("Start Date cannot be null or empty.", responseString);
Steve
  • 2,963
  • 15
  • 61
  • 133
  • 3
    `Assert.Throws` has a return type – Lei Yang Jul 06 '21 at 06:21
  • @LeiYang I updated my answer. But still doesn't work. – Steve Jul 06 '21 at 06:39
  • 1
    ^^ it returns the exception instance on which you then can assert its message. Btw: I personally fell in love with "FluentAssertions" lib. It makes coming up with that kind of asserts really tester friendly. (Not affiliated) – Fildor Jul 06 '21 at 06:41
  • 1
    @Steve You need to `await` it. – Dai Jul 06 '21 at 06:41
  • @Fildor `Shouldly` is better :) – Dai Jul 06 '21 at 06:41
  • @Dai Will not argue about that. I think we can agree that any of those is better than the vanilla assertions of xUnit? – Fildor Jul 06 '21 at 06:43
  • 1
    @Fildor oh, indubitably! `Assert.ShouldlyOrFluentAssertionsIsBetterThanXUnitAssert()` :) – Dai Jul 06 '21 at 06:44
  • Okay, will try `Shouldy` and `FluentAssertions` next time. Now just want to get this Xunit version to work first. – Steve Jul 06 '21 at 07:05
  • @Steve That sounds reasonable. – Fildor Jul 06 '21 at 07:06
  • 2
    @Steve The API method makes this kind of x-y. The method doesn't throw anything. So even if your assertions were 100% correct, they would fail, because your testee does not behave as you expect it to. – Fildor Jul 06 '21 at 07:08
  • 1
    ^^ So you have two options: Assert the actual behavior or refactor the method to behave as expected. I'd consider the arguments in @Dai's answer, though. – Fildor Jul 06 '21 at 07:11
  • @Fildor, not really understand Dai's answer lol. Which means if my argument is null, it should just return bad request and not argumentnullexception? – Steve Jul 06 '21 at 07:15
  • 3
    @Steve Your code is throwing `ArgumentNullException` when `campaign` is **not `null`** (otherwise it cannot dereference `StartDate.Equals`) - that's just flat-out _wrong_. Please read-up on how to correctly represent and enforce preconditions. – Dai Jul 06 '21 at 07:18
  • 1
    @Fildor , I updated my answer. I move the null checking at api layer and updated my test and it works now. – Steve Jul 06 '21 at 07:38
  • 1
    @Steve What do you mean by "api layer"? "API" is a very vague term thanks to everyone abusing when they should be using the term _web service_. \*sigh\* – Dai Jul 06 '21 at 07:39
  • I think from my understand after what you guys said, Web api doesn't throw exception but only error like bad request or internal server error. In my case, should be bad request. But if I test the service directly (without going thru Web Api), then I can use Assert.Throw to check the exception type. Is my understand correct? – Steve Jul 06 '21 at 07:42

2 Answers2

6

Assert.ThrowsAsync is still an async Task method, so you need to await that to ensure the task continuation (that does the actual assert) can run correctly:

[Fact]
public async Task AddCampaign_Return_bad_request_when_date_is_invalid()
{
    [...]

    Assert.False(response.IsSuccessStatusCode);
    await Assert.ThrowsAsync<ArgumentNullException>(()=> client.PostAsync("/api/Campaign/add", encodedContent));
}

However...

  • ....please reconsider your design: Exceptions should be exceptional.
  • Even if you want to throw, you should not be throwing ArgumentNullException to represent HTTP 400 Bad Request responses.
    • The ArgumentException class and its subclasses (ArgumentNullException, ArgumentOutOfRangeException, etc) should only be used to indicate a failed precondition - not a failed postcondition nor internal error (use InvalidOperationException for that).
    • Personally I don't think that web-service clients should ever throw exceptions for any response unless it's actually an "exceptional" response or situation.
    • If you use NSwag to generate web-service clients then it will generate ApiException<TResponse> for you, which is far more useful.
    • Though my preference is to return a discriminated-union of all reasonable possible responses (namely, whatever is declared by [ProducesResponseType]).
Dai
  • 141,631
  • 28
  • 261
  • 374
  • I'd suggest to add the part where you assign the return to a var and assert the message, just to completely answer the question? – Fildor Jul 06 '21 at 06:46
  • 1
    @Fildor Better idea: the OP should not be using `ArgumentNullException` for this - and we should not be testing `Exception.Message` because it's localizable. Furthermore, non-exceptional situations should not be represented by exceptions. – Dai Jul 06 '21 at 06:48
  • While I agree with your comment, the question was _how to do it_. So if another SO user is to look for it (maybe for a better reason), they shall find a useful answer to _that_ , don't they? – Fildor Jul 06 '21 at 06:50
  • I tried but still got errror. I have added my api method. Is it because of my try catch return ` return Problem(ex.Message);`? – Steve Jul 06 '21 at 06:50
  • 1
    @Steve How do you expect the method to throw anything, if you catch any exception it _could_ throw? – Fildor Jul 06 '21 at 06:52
  • 2
    @Steve `Assert.Throws` can only assert an **uncaught exception**. `Assert.Throws` does not verify an exception was thrown-and-caught. For that you need to hook into `AppDomain`, which you should not do in tests like these. – Dai Jul 06 '21 at 06:52
  • What is failed pre condition and post condition? – Steve Jul 06 '21 at 07:24
  • 3
    @Steve http://www.cs.albany.edu/~sdc/CSI310/MainSavage/notes01.pdf https://en.wikipedia.org/wiki/Precondition – Dai Jul 06 '21 at 07:26
4

You can capture an exception with Record.Exception and assert it:

// Act
Action action = async () => await client.PostAsync("/api/Campaign/add", encodedContent);
var ex = Record.Exception(action);

// Assert
Assert.NotNull(ex);
Assert.IsType<ArgumentNullException>(ex);
Mohsen Esmailpour
  • 11,224
  • 3
  • 45
  • 66
  • Not sure if the second snippet is correct regarding `async` ? – Fildor Jul 06 '21 at 07:04
  • @mohsen, I tried your code (the second one), but still doesn't work. Assert.IsType() Failure. Expected: System.ArgumentNullException. Actual: (null) – Steve Jul 06 '21 at 07:04
  • I wanted to make up for the down. Forgetting an "await" ... you can drop a comment or simply edit it in. A downvote without comment is just unfair in that case. – Fildor Jul 06 '21 at 07:05
  • @Steve If you did not reove the try/catch, your method still does not throw anything :D – Fildor Jul 06 '21 at 07:05
  • Wondering how `Assert.NotNull` passed but `Assert.IsType()` threw `System.ArgumentNullException. Actual: (null)`? @Steve – Mohsen Esmailpour Jul 06 '21 at 07:08
  • 2
    @MohsenEsmailpour His API Method doesn't actually throw any exception. – Fildor Jul 06 '21 at 07:09