Java
Mutation Testing
Software Testing
Code Quality
Dependency Injection

Mutation not killed when it should be with a method with an auto-injected field

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If a mutation survives in code that uses an auto-injected field, the usual reason is not that mutation testing is broken. It is that the tests do not make the dependency's effect observable enough to fail when the mutant changes behavior.

Why Auto-Injected Fields Cause Confusion

Field injection hides part of the object's state outside the constructor. That makes tests less explicit and can produce situations where:

  • the dependency is a mock with overly broad stubbing
  • the method under test does not assert on the dependency's effect
  • the mutated line is effectively irrelevant to the final assertion
  • the mutant is equivalent for the exercised scenario

A surviving mutant is a signal that the test did not prove the behavior strongly enough.

Example Of The Problem

Consider a Spring-style service with field injection:

java
1@Service
2public class BillingService {
3
4    @Autowired
5    private DiscountRepository discountRepository;
6
7    public int finalPrice(String customerId, int basePrice) {
8        int discount = discountRepository.findDiscount(customerId);
9        return basePrice - discount;
10    }
11}

Suppose a mutation changes basePrice - discount to basePrice + discount, or removes the repository call entirely. If the test only checks that the result is non-null or greater than zero, the mutant may survive.

java
1@Test
2void finalPriceShouldReturnSomething() {
3    BillingService service = new BillingService();
4    ReflectionTestUtils.setField(service, "discountRepository", repo);
5
6    when(repo.findDiscount("c1")).thenReturn(10);
7
8    assertTrue(service.finalPrice("c1", 100) > 0);
9}

That test is too weak. Both the real implementation and several mutants can still satisfy it.

Make The Behavior Observable

A mutation gets killed when the test makes the changed behavior visible. For the example above, assert the exact result:

java
1@Test
2void finalPriceSubtractsRepositoryDiscount() {
3    BillingService service = new BillingService();
4    ReflectionTestUtils.setField(service, "discountRepository", repo);
5
6    when(repo.findDiscount("c1")).thenReturn(10);
7
8    assertEquals(90, service.finalPrice("c1", 100));
9}

Now a mutant that changes subtraction to addition is much more likely to die.

Prefer Constructor Injection

Field injection also makes tests awkward because you need Spring context bootstrapping or reflection-based field setting. Constructor injection is better for mutation testing and unit testing in general:

java
1public class BillingService {
2    private final DiscountRepository discountRepository;
3
4    public BillingService(DiscountRepository discountRepository) {
5        this.discountRepository = discountRepository;
6    }
7
8    public int finalPrice(String customerId, int basePrice) {
9        int discount = discountRepository.findDiscount(customerId);
10        return basePrice - discount;
11    }
12}

Test:

java
1@Test
2void finalPriceSubtractsRepositoryDiscount() {
3    DiscountRepository repo = mock(DiscountRepository.class);
4    when(repo.findDiscount("c1")).thenReturn(10);
5
6    BillingService service = new BillingService(repo);
7
8    assertEquals(90, service.finalPrice("c1", 100));
9}

This version makes the dependency explicit and eliminates hidden wiring from the test.

Surviving Mutants Are Not Always Test Bugs

Sometimes the mutant is equivalent, meaning it changes the code syntactically but not behaviorally for that path. That can happen with:

  • dead code
  • redundant null checks
  • logging-only branches
  • framework-generated wiring that tests do not care about

But do not assume equivalence too quickly. Most of the time, a surviving mutant around an injected field means the test is not asserting the right outcome or interaction.

Verify Interactions When They Matter

If the important behavior is that a collaborator must be called, assert that too:

java
1@Test
2void finalPriceUsesRepositoryLookup() {
3    DiscountRepository repo = mock(DiscountRepository.class);
4    when(repo.findDiscount("c1")).thenReturn(10);
5
6    BillingService service = new BillingService(repo);
7    service.finalPrice("c1", 100);
8
9    verify(repo).findDiscount("c1");
10}

Interaction assertions should not replace result assertions, but they can kill mutants that bypass collaborator calls.

Reduce Framework Noise In Unit Tests

Mutation testing works best when unit tests exercise plain objects with explicit dependencies. If the test spins up a Spring container for every case, it becomes slower and the actual behavioral contract can be harder to see.

A good rule is:

  • use constructor injection in the production class
  • test the class without Spring when possible
  • assert exact outputs or exact side effects
  • use interaction verification only for meaningful collaborator behavior

That makes surviving mutants easier to interpret.

Common Pitfalls

  • Using field injection and then patching tests with reflection instead of improving the design.
  • Writing assertions that are too broad, such as non-null or positive checks.
  • Mocking collaborators in a way that hides whether the mutated code path was really used.
  • Assuming every surviving mutant is equivalent without proving it.
  • Testing through the full framework when a focused unit test would expose the behavior more clearly.

Summary

  • A surviving mutant around an auto-injected field usually means the dependency's effect is not asserted strongly enough.
  • Constructor injection makes mutation testing and ordinary unit testing much clearer.
  • Assert exact outputs and important collaborator interactions.
  • Treat equivalent mutants as the exception, not the default explanation.
  • If a mutant survives, ask what observable behavior should have changed and why the test did not catch it.

Course illustration
Course illustration

All Rights Reserved.