Skip to content

Refactor test method naming to follow the .NET unit testing standard #396

@nanotaboada

Description

@nanotaboada

Problem

The current test method naming follows a GWT-ish pattern (Given{Method}_When{Condition}_Then{Outcome}) that is both more verbose than necessary for unit tests and misaligned with the Microsoft-endorsed industry standard for .NET unit test naming.

Current examples:

GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_ThenResponseShouldBeEquivalentToListOfPlayers
GivenPostAsync_WhenValidatorReturnsErrors_ThenResponseStatusCodeShouldBe400BadRequest
GivenValidateAsync_WhenDateOfBirthIsInTheFuture_ThenValidationShouldFail

These names carry ~40% filler words (Given, When, Then, ShouldBe) that add length without adding meaning.

Research

Microsoft official standard

Unit testing best practices for .NET (Microsoft Learn) explicitly defines test names as having three parts:

Name of the method being tested · Scenario under which it is tested · Expected behavior when the scenario is invoked

Their canonical pattern (based on Roy Osherove's Naming Standards for Unit Tests):

MethodName_StateUnderTest_ExpectedBehavior

Examples from their docs:

Add_SingleNumber_ReturnsSameNumber()
Add_EmptyString_ReturnsZero()
GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()

Where GWT belongs

Given/When/Then originates in BDD (Cucumber, SpecFlow, Gherkin) and is appropriate for acceptance/integration tests that map directly to user stories. For unit tests — where the subject is a single method or class — it adds noise. The three-part Microsoft standard conveys the same information without the filler.

Adapting the standard to HTTP controller tests

For controller tests, the unit of work is always an HTTP request/response cycle. Encoding the HTTP verb and resource makes the three parts concrete and immediately scannable, while remaining a faithful expression of UnitOfWork_StateUnderTest_ExpectedBehavior:

{METHOD}_{Resource}_{Condition}_Returns{Outcome}

This is still the same Microsoft-standard three-part structure — just with HTTP semantics making each part explicit.

Proposed Solution

Apply a unified naming pattern derived from the Microsoft standard, specialized by test layer:

Layer Pattern Rationale
Controller {METHOD}_{Resource}_{Condition}_Returns{Outcome} HTTP semantics make the three parts explicit
Service {MethodName}_{StateUnderTest}_{ExpectedBehavior} Pure Microsoft standard; no HTTP concerns
Validator {MethodName}_{StateUnderTest}_{ExpectedBehavior} Pure Microsoft standard; no HTTP concerns

Suggested Approach

Controller tests (PlayerControllerTests.cs)

Before After
GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_ThenResponseShouldBeEquivalentToListOfPlayers GET_Players_Existing_ReturnsPlayers
GivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenResponseStatusCodeShouldBe404NotFound GET_Players_NonExisting_Returns404NotFound
GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200Ok GET_PlayerById_Existing_Returns200OK
GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound GET_PlayerById_NonExisting_Returns404NotFound
GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200Ok GET_PlayerBySquadNumber_Existing_Returns200OK
GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound GET_PlayerBySquadNumber_NonExisting_Returns404NotFound
GivenPostAsync_WhenValidatorReturnsErrors_ThenResponseStatusCodeShouldBe400BadRequest POST_Players_ValidationError_Returns400BadRequest
GivenPostAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe409Conflict POST_Players_Existing_Returns409Conflict
GivenPostAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe201Created POST_Players_NonExisting_Returns201Created
GivenPutAsync_WhenValidatorReturnsErrors_ThenResponseStatusCodeShouldBe400BadRequest PUT_PlayerBySquadNumber_ValidationError_Returns400BadRequest
GivenPutAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound PUT_PlayerBySquadNumber_NonExisting_Returns404NotFound
GivenPutAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContent PUT_PlayerBySquadNumber_Existing_Returns204NoContent
GivenDeleteAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound DELETE_PlayerBySquadNumber_NonExisting_Returns404NotFound
GivenDeleteAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContent DELETE_PlayerBySquadNumber_Existing_Returns204NoContent

Service tests (PlayerServiceTests.cs)

Before After
GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToRepositoryAndRemovesCache CreateAsync_WhenCalled_AddsPlayerAndRemovesCache
GivenRetrieveAsync_WhenRepositoryGetAllAsyncReturnsPlayers_ThenCacheCreateEntryAndResultShouldBeListOfPlayers RetrieveAsync_CacheMiss_QueriesRepositoryAndCachesResult
GivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExecutionTimeShouldBeLessThanFirst RetrieveAsync_CacheHit_ReturnsBeforeQueryingRepository
GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsNull_TheResultShouldBeNull RetrieveByIdAsync_PlayerNotFound_ReturnsNull
GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_TheResultShouldBePlayer RetrieveByIdAsync_PlayerFound_ReturnsMappedResponseModel
GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsNull_ThenResultShouldBeNull RetrieveBySquadNumberAsync_PlayerNotFound_ReturnsNull
GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenResultShouldBePlayer RetrieveBySquadNumberAsync_PlayerFound_ReturnsMappedResponseModel
GivenUpdateAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenRepositoryUpdateAsyncAndCacheRemove UpdateAsync_PlayerFound_UpdatesRepositoryAndRemovesCache
GivenDeleteAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenRepositoryDeleteAsyncAndCacheRemove DeleteAsync_PlayerFound_RemovesFromRepositoryAndRemovesCache

Validator tests (PlayerValidatorTests.cs)

Before After
GivenValidateAsync_WhenRequestModelIsValid_ThenValidationShouldPass ValidateAsync_ValidRequest_ReturnsNoErrors
GivenValidateAsync_WhenFirstNameIsEmpty_ThenValidationShouldFail ValidateAsync_FirstNameEmpty_ReturnsValidationError
GivenValidateAsync_WhenLastNameIsEmpty_ThenValidationShouldFail ValidateAsync_LastNameEmpty_ReturnsValidationError
GivenValidateAsync_WhenSquadNumberIsNotGreaterThanZero_ThenValidationShouldFail ValidateAsync_SquadNumberNotGreaterThanZero_ReturnsValidationError
GivenValidateAsync_WhenSquadNumberIsNotUnique_ThenValidationShouldFail ValidateAsync_SquadNumberNotUnique_ReturnsValidationError
GivenValidateAsync_WhenAbbrPositionIsEmpty_ThenValidationShouldFail ValidateAsync_AbbrPositionEmpty_ReturnsValidationError
GivenValidateAsync_WhenAbbrPositionIsInvalid_ThenValidationShouldFail ValidateAsync_AbbrPositionInvalid_ReturnsValidationError
GivenValidateAsync_WhenDateOfBirthIsNull_ThenValidationShouldPass ValidateAsync_DateOfBirthNull_ReturnsNoErrors
GivenValidateAsync_WhenDateOfBirthIsInTheFuture_ThenValidationShouldFail ValidateAsync_DateOfBirthInFuture_ReturnsValidationError
GivenValidateAsync_WhenDateOfBirthIsBeforeYear1900_ThenValidationShouldFail ValidateAsync_DateOfBirthBeforeYear1900_ReturnsValidationError

Acceptance Criteria

  • All PlayerControllerTests.cs methods follow {METHOD}_{Resource}_{Condition}_Returns{Outcome}
  • All PlayerServiceTests.cs methods follow {MethodName}_{StateUnderTest}_{ExpectedBehavior}
  • All PlayerValidatorTests.cs methods follow {MethodName}_{StateUnderTest}_{ExpectedBehavior}
  • All 33 tests pass after renaming

References

  • Unit testing best practices for .NET — Microsoft Learn (official)
  • Naming Standards for Unit Tests — Roy Osherove (cited by Microsoft; origin of UnitOfWork_StateUnderTest_ExpectedBehavior)
  • test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs
  • test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs
  • test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs

Metadata

Metadata

Assignees

No one assigned

    Labels

    dotnetPull requests that update .NET codeenhancementNew feature or requestrefactor

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions