4

EnsureSuccessStatusCode as an assertion

 3 years ago
source link: https://blog.ploeh.dk/2020/09/28/ensuresuccessstatuscode-as-an-assertion/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

It finally dawned on me that EnsureSuccessStatusCode is a fine test assertion.

Okay, I admit that sometimes I can be too literal and rigid in my thinking. At least as far back as 2012 I've been doing outside-in TDD against self-hosted HTTP APIs. When you do that, you'll regularly need to verify that an HTTP response is successful.

Assert equality #

You can, of course, write an assertion like this:

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

If the assertion fails, it'll produce a comprehensible message like this:

Assert.Equal() Failure
Expected: OK
Actual:   BadRequest

What's not to like?

Assert success #

The problem with testing strict equality in this context is that it's often too narrow a definition of success.

You may start by returning 200 OK as response to a POST request, but then later evolve the API to return 201 Created or 202 Accepted. Those status codes still indicate success, and are typically accompanied by more information (e.g. a Location header). Thus, evolving a REST API from returning 200 OK to 201 Created or 202 Accepted isn't a breaking change.

It does, however, break the above assertion, which may now fail like this:

Assert.Equal() Failure
Expected: OK
Actual:   Created

You could attempt to fix this issue by instead verifying IsSuccessStatusCode:

Assert.True(response.IsSuccessStatusCode);

That permits the API to evolve, but introduces another problem.

Legibility #

What happens when an assertion against IsSuccessStatusCode fails?

Assert.True() Failure
Expected: True
Actual:   False

That's not helpful. At least, you'd like to know which other status code was returned instead. You can address that concern by using an overload to Assert.True (most unit testing frameworks come with such an overload):

Assert.True(
    response.IsSuccessStatusCode,
    $"Actual status code: {response.StatusCode}.");

This produces more helpful error messages:

Actual status code: BadRequest.
Expected: True
Actual:   False

Granted, the Expected: True, Actual: False lines are redundant, but at least the information you care about is there; in this example, the status code was 400 Bad Request.

Conciseness #

That's not bad. You can use that Assert.True overload everywhere you want to assert success. You can even copy and paste the code, because it's an idiom that doesn't change. Still, since I like to stay within 80 characters line width, this little phrase takes up three lines of code.

Surely, a helper method can address that problem:

internal static void AssertSuccess(this HttpResponseMessage response)
{
    Assert.True(
        response.IsSuccessStatusCode,
        $"Actual status code: {response.StatusCode}.");
}

I can now write a one-liner as an assertion:

response.AssertSuccess();

What's not to like?

Carrying coals to Newcastle #

If you're already familiar with the HttpResponseMessage class, you may have been reading this far with some frustration. It already comes with a method called EnsureSuccessStatusCode. Why not just use that instead?

response.EnsureSuccessStatusCode();

As long as the response status code is in the 200 range, this method call succeeds, but if not, it throws an exception:

Response status code does not indicate success: 400 (Bad Request).

That's exactly the information you need.

I admit that I can sometimes be too rigid in my thinking. I've known about this method for years, but I never thought of it as a unit testing assertion. It's as if there's a category in my programming brain that's labelled unit testing assertions, and it only contains methods that originate from unit testing.

I even knew about Debug.Assert, so I knew that assertions also exist outside of unit testing. That assertion isn't suitable for unit testing, however, so I never included it in my category of unit testing assertions.

The EnsureSuccessStatusCode method doesn't have assert in its name, but it behaves just like a unit testing assertion: it throws an exception with a useful error message if a criterion is not fulfilled. I just never thought of it as useful for unit testing purposes because it's part of 'production code'.

Conclusion #

The built-in EnsureSuccessStatusCode method is fine for unit testing purposes. Who knows, perhaps other class libraries contain similar assertion methods, although they may be hiding behind names that don't include assert. Perhaps your own production code contains such methods. If so, they can double as unit testing assertions.

I've been doing test-driven development for close to two decades now, and one of many consistent experiences is this:

  • In the beginning of a project, the unit tests I write are verbose. This is natural, because I'm still getting to know the domain.
  • After some time, I begin to notice patterns in my tests, so I refactor them. I introduce helper APIs in the test code.
  • Finally, I realise that some of those unit testing APIs might actually be useful in the production code too, so I move them over there.
Test-driven development is process that gives you feedback about your production code's APIs. Listen to your tests.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK