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
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
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:
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:
Their canonical pattern (based on Roy Osherove's Naming Standards for Unit Tests):
Examples from their docs:
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: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:
{METHOD}_{Resource}_{Condition}_Returns{Outcome}{MethodName}_{StateUnderTest}_{ExpectedBehavior}{MethodName}_{StateUnderTest}_{ExpectedBehavior}Suggested Approach
Controller tests (
PlayerControllerTests.cs)GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_ThenResponseShouldBeEquivalentToListOfPlayersGET_Players_Existing_ReturnsPlayersGivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenResponseStatusCodeShouldBe404NotFoundGET_Players_NonExisting_Returns404NotFoundGivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200OkGET_PlayerById_Existing_Returns200OKGivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFoundGET_PlayerById_NonExisting_Returns404NotFoundGivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200OkGET_PlayerBySquadNumber_Existing_Returns200OKGivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFoundGET_PlayerBySquadNumber_NonExisting_Returns404NotFoundGivenPostAsync_WhenValidatorReturnsErrors_ThenResponseStatusCodeShouldBe400BadRequestPOST_Players_ValidationError_Returns400BadRequestGivenPostAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe409ConflictPOST_Players_Existing_Returns409ConflictGivenPostAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe201CreatedPOST_Players_NonExisting_Returns201CreatedGivenPutAsync_WhenValidatorReturnsErrors_ThenResponseStatusCodeShouldBe400BadRequestPUT_PlayerBySquadNumber_ValidationError_Returns400BadRequestGivenPutAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFoundPUT_PlayerBySquadNumber_NonExisting_Returns404NotFoundGivenPutAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContentPUT_PlayerBySquadNumber_Existing_Returns204NoContentGivenDeleteAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFoundDELETE_PlayerBySquadNumber_NonExisting_Returns404NotFoundGivenDeleteAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContentDELETE_PlayerBySquadNumber_Existing_Returns204NoContentService tests (
PlayerServiceTests.cs)GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToRepositoryAndRemovesCacheCreateAsync_WhenCalled_AddsPlayerAndRemovesCacheGivenRetrieveAsync_WhenRepositoryGetAllAsyncReturnsPlayers_ThenCacheCreateEntryAndResultShouldBeListOfPlayersRetrieveAsync_CacheMiss_QueriesRepositoryAndCachesResultGivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExecutionTimeShouldBeLessThanFirstRetrieveAsync_CacheHit_ReturnsBeforeQueryingRepositoryGivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsNull_TheResultShouldBeNullRetrieveByIdAsync_PlayerNotFound_ReturnsNullGivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_TheResultShouldBePlayerRetrieveByIdAsync_PlayerFound_ReturnsMappedResponseModelGivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsNull_ThenResultShouldBeNullRetrieveBySquadNumberAsync_PlayerNotFound_ReturnsNullGivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenResultShouldBePlayerRetrieveBySquadNumberAsync_PlayerFound_ReturnsMappedResponseModelGivenUpdateAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenRepositoryUpdateAsyncAndCacheRemoveUpdateAsync_PlayerFound_UpdatesRepositoryAndRemovesCacheGivenDeleteAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenRepositoryDeleteAsyncAndCacheRemoveDeleteAsync_PlayerFound_RemovesFromRepositoryAndRemovesCacheValidator tests (
PlayerValidatorTests.cs)GivenValidateAsync_WhenRequestModelIsValid_ThenValidationShouldPassValidateAsync_ValidRequest_ReturnsNoErrorsGivenValidateAsync_WhenFirstNameIsEmpty_ThenValidationShouldFailValidateAsync_FirstNameEmpty_ReturnsValidationErrorGivenValidateAsync_WhenLastNameIsEmpty_ThenValidationShouldFailValidateAsync_LastNameEmpty_ReturnsValidationErrorGivenValidateAsync_WhenSquadNumberIsNotGreaterThanZero_ThenValidationShouldFailValidateAsync_SquadNumberNotGreaterThanZero_ReturnsValidationErrorGivenValidateAsync_WhenSquadNumberIsNotUnique_ThenValidationShouldFailValidateAsync_SquadNumberNotUnique_ReturnsValidationErrorGivenValidateAsync_WhenAbbrPositionIsEmpty_ThenValidationShouldFailValidateAsync_AbbrPositionEmpty_ReturnsValidationErrorGivenValidateAsync_WhenAbbrPositionIsInvalid_ThenValidationShouldFailValidateAsync_AbbrPositionInvalid_ReturnsValidationErrorGivenValidateAsync_WhenDateOfBirthIsNull_ThenValidationShouldPassValidateAsync_DateOfBirthNull_ReturnsNoErrorsGivenValidateAsync_WhenDateOfBirthIsInTheFuture_ThenValidationShouldFailValidateAsync_DateOfBirthInFuture_ReturnsValidationErrorGivenValidateAsync_WhenDateOfBirthIsBeforeYear1900_ThenValidationShouldFailValidateAsync_DateOfBirthBeforeYear1900_ReturnsValidationErrorAcceptance Criteria
PlayerControllerTests.csmethods follow{METHOD}_{Resource}_{Condition}_Returns{Outcome}PlayerServiceTests.csmethods follow{MethodName}_{StateUnderTest}_{ExpectedBehavior}PlayerValidatorTests.csmethods follow{MethodName}_{StateUnderTest}_{ExpectedBehavior}References
UnitOfWork_StateUnderTest_ExpectedBehavior)test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cstest/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cstest/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs