0

I recently cam across a class like this:

public class SomeDao {
   @Inject
   private DataSource dataSource;

   @Value("#{someMap['someDao.sql']}")
   private String sql;

   private JdbcTemplate jdbcTemplate;

   @PostConstruct
   private void postConstruct() {
      jdbcTemplate = new JdbcTemplate(dataSource);
   }

   ...
}

Now I would like to unit test this class by injecting the DataSource and the SQL string. As far as I can see, there are two options for this:

  1. Load up an application context which will be slow and most likely load a whole lot of stuff that I don't need for the test
  2. Use reflection to set the private properties and call the private postConstruct() method

In the days before spring annotations, the class would have been written like this:

public class SomeDao {
   private String sql;
   private JdbcTemplate jdbcTemplate;

   public SomeDao(DataSource dataSource, String sql) {
      this.jdbcTemplate = new JdbcTemplate(dataSource);
      this.sql = sql;
   }    
   ...
}

And I could have easily tested this class without the need for reflection or spring. My class was a pure pojo and had no spring dependencies.

So, are spring annotations a good thing or are they a step backwards? Are there times where I shoulud be using them and times when I should use the old XML app context?

Thanks, Lance.

Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
lance-java
  • 25,497
  • 4
  • 59
  • 101

4 Answers4

4

Why not declare a test-context with mocks of beans and inject those needed by your class from that? That's what people usually do and it's quite straightforward.

The most lightweight way of doing this is to provide an inner class inside your test class, annotated with @Configuration that has methods that provide mocks:

@Configuration
public class DataAccessConfiguration {

    @Bean
    public DataSource dataSource() {
        DataSource dataSource =  mock(Datasource.class);
        return dataSource;
    }

    @Bean
    public Map<String,String> someMap() {
        Map<String, String> map = new HashMap<>();
        map.put("someDao.sql", "somevalue");
        return map;
    }

}

So instead of giving up on autowiring, you can actually take advantage of it. Also you limit the loaded context to just what is needed by the class under test.

soulcheck
  • 36,297
  • 6
  • 91
  • 90
3

I'd say there is an unhealthy tendency to prefer setter injection (or even injection into private fields) over constructor injection even for required dependencies.

Note that since both dependncies in this example are required it can be rewritten as follows:

public class SomeDao {
    private String sql;
    private JdbcTemplate jdbcTemplate;

    @Inject  
    public SomeDao(
        DataSource dataSource, 
        @Value("#{someMap['someDao.sql']}") String sql) {

        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.sql = sql;
    }   
    ...
}

So, it's not a problem with Spring annotations, it's a problem with incorrect use of them.

axtavt
  • 239,438
  • 41
  • 511
  • 482
  • Can you provide some reasoning as to why you find setter/field inject to be "unhealthy"? It seems to me that--when you have easy alternatives like Mockito or the solution soulcheck posted--adding a constructor for elements you have already listed is essentially code duplication and, therefore, simply clutter. I'm having trouble thinking of a *single* good reason *not* to use field injection. It's just as fast in production, faster to write, has less clutter, it's clearer... So why do you dislike it so? – Tim Pote May 12 '12 at 01:27
  • @TimPote: I can think of several reasons against field injection: cannot be tested without reflection or frameworks such as spring; does not make your dependencies explicit; it is too easy to create _god classes_; you can easily miss the smell of "too many dependencies"; fields cannot be `final`, even though they should be immutable; … – knittl Apr 25 '20 at 16:16
0

You can still use annotations on setters and constructors, which i personally prefer as it makes the dependencies much more obvious. Those 50 parameter constructors really stand out.

blank
  • 17,852
  • 20
  • 105
  • 159
0

While I agree with axtavt's answer, a quick workaround is to use the ReflectionTestUtils class from the spring-test artifact. It provides nice one-liners for such scenarios:

ReflectionTestUtils.setField(dao, "dataSource", mydataSource);

For unit tests, that's about as simple as it gets.

See ReflectionTestUtils

Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
  • I really dislike this approach and I feel it's hacky. I would also need to reflectively invoke the postConstruct() method too. Again, hacky. – lance-java May 04 '12 at 07:30
  • I know, I don't like private annotated fields at all for this reason. But it's the simplest way to deal with that problem. – Sean Patrick Floyd May 04 '12 at 07:45
  • I disagree, soulcheck has provided an excellent way of using a @Configuration to inject the dependencies. It's much cleaner than manual reflection. – lance-java May 04 '12 at 12:34
  • @uklance It's nice and certainly cleaner, but I'd still say my way is easier. – Sean Patrick Floyd May 04 '12 at 12:39
  • Reflection does not refactor when you refactor your code. ReflectionTestUtils also does not allow me to invoke private methods. – lance-java May 08 '12 at 15:17
  • @uklance true, but using a Spring container makes it an integration test where a unit test would suffice. I'd say both ways are valid – Sean Patrick Floyd May 08 '12 at 15:57
  • Such a silly distinction to get hung up on. I'll still call it a unit test even though spring instantiated the object under test. It's a clean solution – lance-java Sep 26 '17 at 12:17
  • 5.5 years have passed. Things change. I would no longer recommend reflection here. If you can, change setter injection to constructor injection and inject a mock manually. If not, use Mockito's InjectMocks. But I stand with what I said: no need to invoke Spring, unless you need a specific functionality for the test (i.e. transactions). – Sean Patrick Floyd Sep 26 '17 at 19:05