Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring @Autowired behavior different in tests than components

Are the rules/behaviors around @Autowired different when writing tests? It seems that with a test, you can autowire to a concrete type, but if you try the same thing inside a @Component it will fail. This is a contrived example, but it's something I ran into and am just trying to understand better.

Contrived example code:

public interface Gizmo {

  void whirr();
}

@Configuration
public class GizmoConfiguration {

  @Bean
  @Profile("no-dependencies")
  public Gizmo fooGizmoBean() {
    return new FooGizmo();
  }

  @Bean
  @Profile("!no-dependencies")
  public Gizmo barGizmoBean() {
    return new BarGizmo();
  }

  public class FooGizmo implements Gizmo {
    @Override
    public void whirr() {
    }
  }

  public class BarGizmo implements Gizmo {
    @Override
    public void whirr() {
    }
  }
}

Test that runs fine:

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles(Application.Profiles.NO_DEPENDENCIES)
public class TestClass {

  @Autowired
  private GizmoConfiguration.FooGizmo gizmo;

  @Test
  public void test() {
    assertNotNull(gizmo);
  }
}

Component that causes java.lang.IllegalStateException: Failed to load ApplicationContext:

@Component
public class TestComponent {

  @Autowired
  private GizmoConfiguration.FooGizmo gizmo;
}

because of:

No qualifying bean of type 'GizmoConfiguration$FooGizmo' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
like image 790
chinabuffet Avatar asked Jun 22 '18 15:06

chinabuffet


Video Answer


1 Answers

Are the rules/behaviors around @Autowired different when writing tests?

Not exactly: the rules are actually exactly the same. The difference is in terms of timing with regard to how Spring determines if a given bean is an autowire candidate.

It seems that with a test, you can autowire to a concrete type, but if you try the same thing inside a @Component it will fail.

I understand why you would think that, since your example demonstrates that behavior, but your analysis is not exactly correct.

So let me explain...

When Spring attempts to perform autowiring for your @Component class, the only information it has about types (i.e., classes and interfaces) for beans coming from @Bean methods is the information available in an @Bean method's formal signature.

In your example, when Spring searches for so-called "autowire candidates" to inject into your @Component, Spring only sees a bean of type Gizmo for your fooGizmoBean() @Bean method. So that's why you see the "No qualifying bean of type 'GizmoConfiguration$FooGizmo'" error, which happens to be completely correct.

If you want Spring to be able to autowire your @Component using the concrete type, you will have to redefine the signature of your fooGizmoBean() @Bean method to return FooGizmo instead of Gizmo.

So, that's the first half of the story. The second half of the story is why the Spring TestContext Framework is able to perform autowiring by the concrete type for the test instance.

The reason that works is that the ApplicationContext has already been completely started (i.e., all beans have been instantiated and all @Bean methods have been invoked by the container) by the time the testing framework attempts to perform dependency injection. By that point in time, the fooGizmoBean() method has already been invoked by Spring, and Spring now knows the concrete type is actually a FooGizmo. Thus, @Autowired FooGizmo gizmo; works in the test.

like image 75
Sam Brannen Avatar answered Sep 18 '22 13:09

Sam Brannen