Test Without Assertion

ID

csharp.test_without_assertion

Severity

critical

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

assertion, reliability, testing

Description

Reports a test method whose body states no expectation at all. The method exercises the code under test and then ends, so the test passes no matter what the code returned.

Assertions are recognised across the frameworks and libraries in common use: the static facades of xUnit, NUnit and MSTest (Assert, ClassicAssert, CollectionAssert, StringAssert, …), the FluentAssertions .Should() style, the Shouldly .ShouldBe(…) family, and the verification calls of Moq, NSubstitute and FakeItEasy.

A suite often factors the expectation out into a helper the test calls instead of stating it inline. When the call is bare or qualified with this — so it can only resolve to a method of this type or a base type — the helper declared in the same file is resolved and its body is checked the same way, one level deep. A helper declared in another file, such as a base class kept in its own compilation unit, is out of reach the same way an assertion in one would be.

Three shapes that also lack an assertion are left alone, because each is a different defect with a different fix: a test with an empty body, a test whose body only throws, and a test declared to expect an exception — there the attribute is the expectation. Tests taken out of the run by [Ignore] or [Explicit] are skipped for the same reason.

This rule applies to test code only.

Rationale

A test that asserts nothing still runs the code under test, so it produces a green tick and a coverage line. Both are misleading: nothing about the result was ever checked, and the test will keep passing after the behaviour it was written to protect has been broken. That is worse than having no test, because the suite reports the area as covered and no one looks again.

The usual causes are a test written to reproduce a crash and never completed once the crash was fixed, a test whose assertion was commented out during debugging, and a generated test skeleton that was filled in with a call but never with an expectation.

[Fact]
public void TotalIsComputed()               // FLAW - nothing is checked
{
    var service = new OrderService();
    var total = service.Total(2, 3);
    Console.WriteLine(total);
}

[Fact]
public void TotalIsFive()                   // OK
{
    var service = new OrderService();
    Assert.Equal(5, service.Total(2, 3));
}

[Fact]
public void OrderIsPersisted()              // OK - a mock verification is an expectation
{
    var repository = new Mock<IOrderRepository>();
    new OrderService(repository.Object).Save(new Order());
    repository.Verify(r => r.Add(It.IsAny<Order>()), Times.Once);
}

[Fact]
public void TotalIsFiveViaHelper()          // OK - the same-file helper it calls asserts
{
    RunTotalScenario(2, 3, 5);
}

private static void RunTotalScenario(int quantity, int price, int expected)
{
    Assert.Equal(expected, new OrderService().Total(quantity, price));
}

[Fact]
public void NotWrittenYet()                 // OK - an empty test is a different defect
{
}

[Fact]
[ExpectedException(typeof(ArgumentException))]
public void RejectsEmptyOrder()             // OK - the attribute is the expectation
{
    new OrderService().Save(null);
}

Remediation

Add the expectation the test was written to state: assert on the value returned, on the state the call left behind, or on the interaction with a collaborator. If the test exists only to prove the call does not throw, say so explicitly with the framework’s own idiom rather than leaving the body bare. If the test is genuinely unfinished, mark it as such so the suite stops counting it as coverage, or delete it.

When the expectation lives in a helper the test calls, the rule follows a bare or this-qualified call into a helper declared in the same file. A helper kept in another file — a base class in its own compilation unit, for instance — is still invisible to the rule; extract the assertion back into the test, move the helper into the same file, or accept the finding as a known limitation.