从使用Spring 2.5开始,我从基于XML的应用程序上下文切换到了注释。 尽管我发现那些非常有用且节省大量时间的人,但我始终觉得在灵活性方面我失去了一些东西。 特别是@Autowired批注-或标准@Inject-在我看来就像新的“新”,增加了类之间的耦合,并使得在需要时更难以更改实现。 我仍然有这种感觉,但是我已经学到了一种有趣的模式来限制测试代码时的问题,即当我想将bean的真实实现替换为模拟时。 让我们用一个例子来说明。 我想构建一个应用程序,以便为我在网络上找到有趣的东西。 我将从一个接受URL的服务开始,如果它是一个有趣的新URL,则将其添加书签。 直到最近,我可能已经编写了如下代码:
@Named
public class AwesomenessFinder {@Injectprivate BlogAnalyzer blogAnalyzer;@Injectprivate BookmarkService bookmarkService;public void checkBlog(String url) {if (!bookmarkService.contains(url) && blogAnalyzer.isInteresting(url)) {bookmarkService.bookmark(url);}}
}
不好,你明白为什么吗? 如果没有,请继续阅读,希望您今天能学到一些有用的东西。 因为我很认真,所以我想为此代码创建单元测试。 希望我的算法很好,但是我想确保它不会为无聊的博客添加书签或将相同的URL添加为书签两次。 那就是问题所在,我想将AwesomenessFinder与它的依赖隔离开来。 如果我使用的是XML配置,则可以在测试上下文中简单地注入模拟实现,是否可以使用批注来实现? 嗯,是! 有一种方法,带有@Primary批注。 让我们尝试为BlogAnalyzer和BookmarkService创建模拟实现。
@Named
@Primary
public class BlogAnalyzerMock implements BlogAnalyzer {public boolean isInteresting(String url) {return true;}
}@Named
@Primary
public class BookmarkServiceMock implements BookmarkService {Set bookmarks = new HashSet();public boolean contains(String url) {return bookmarks.contains(url);}public void bookmark(String url) {bookmarks.add(url);}
}
因为我使用Maven并将这些模拟放置在test / java目录中,所以主应用程序将看不到它们,并将注入实际的实现。 另一方面,单元测试将看到2种实现。 @Primary是必需的,以防止出现类似以下的异常:
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No unique bean of type [service.BlogAnalyzer] is defined: expected single matching bean
but found 2: [blogAnalyzerMock, blogAnalyzerImpl]
现在,我可以测试我的算法了:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:application-context.xml")
public class AwesomenessFinderTest {@Injectprivate AwesomenessFinder awesomenessFinder;@Injectprivate BookmarkService bookmarkService;@Testpublic void checkInterestingBlog_bookmarked() {String url = "http://www.javaspecialists.eu";assertFalse(bookmarkService.contains(url));awesomenessFinder.checkBlog(url);assertTrue(bookmarkService.contains(url));}
}
不错,我测试了幸福的道路,一个有趣的博客被加了书签。 现在我该如何测试其他情况。 当然,我可以在模拟中添加一些逻辑,以查找某些已添加书签或不感兴趣的URL,但这会变得笨拙。 这是一个非常简单的算法,想象一下测试更复杂的东西有多糟糕。 有一种更好的方法需要重新设计我的类以及注入依赖项的方法。 方法如下:
@Named
public class AwesomenessFinder {private BlogAnalyzer blogAnalyzer;private BookmarkService bookmarkService;@Injectpublic AwesomenessFinder(BlogAnalyzer blogAnalyzer, BookmarkService bookmarkService) {this.blogAnalyzer = blogAnalyzer;this.bookmarkService = bookmarkService;}public void checkBlog(String url) {if (!bookmarkService.contains(url) && blogAnalyzer.isInteresting(url)) {bookmarkService.bookmark(url);}}
}
请注意,我仍然使用@Inject注释自动关联我的依赖项,因此AwesomenessFinder的调用者不会受到影响。 例如,客户端类中的以下内容仍然有效:
@Inject
private AwesomenessFinder awesomenessFinder;
但是,最大的不同是我在构造函数级别自动装配,这为我提供了一种注入模拟实现的干净方法。 而且,由于我们是在模拟,所以我们使用一个模拟库。 去年,我写了一篇有关嘲讽的文章,其中我使用了丑陋的二传手来注入嘲讽。 使用这里提到的技术,我不再需要暴露依赖项,我得到了更好的封装。 这是更新后的测试用例的样子:
public class AwesomenessFinderTest {@Testpublic void checkInterestingBlog_bookmarked() {BookmarkService bookmarkService = mock(BookmarkService.class);when(bookmarkService.contains(anyString())).thenReturn(false);BlogAnalyzer blogAnalyzer = mock(BlogAnalyzer.class);when(blogAnalyzer.isInteresting(anyString())).thenReturn(true);AwesomenessFinder awesomenessFinder = new AwesomenessFinder(blogAnalyzer, bookmarkService);String url = "http://www.javaspecialists.eu";awesomenessFinder.checkBlog(url);verify(bookmarkService).bookmark(url);}
}
请注意,现在这是纯Java语言,无需使用Spring注入模拟。 而且,这些模拟的定义与它们的用法位于同一位置,从而简化了维护。 为了更进一步,让我们实现其他测试用例。 为了避免代码重复,我们将重构测试类并引入一些枚举,以使测试用例尽可能地表达。
public class AwesomenessFinderTest {private enum Knowledge {KNOWN, UNKNOWN};private enum Quality {INTERESTING, BORING};private enum ExpectedBookmark {STORED, IGNORED}private enum ExpectedAnalysis {ANALYZED, SKIPPED}@Testpublic void checkInterestingBlog_bookmarked() {checkCase(Knowledge.UNKNOWN, Quality.INTERESTING,ExpectedBookmark.STORED, ExpectedAnalysis.ANALYZED);}@Testpublic void checkBoringBlog_ignored() {checkCase(Knowledge.UNKNOWN, Quality.BORING,ExpectedBookmark.IGNORED, ExpectedAnalysis.ANALYZED);}@Testpublic void checkKnownBlog_ignored() {checkCase(Knowledge.KNOWN, Quality.INTERESTING,ExpectedBookmark.IGNORED, ExpectedAnalysis.SKIPPED);}private void checkCase(Knowledge knowledge, Quality quality,ExpectedBookmark expectedBookmark, ExpectedAnalysis expectedAnalysis) {BookmarkService bookmarkService = mock(BookmarkService.class);boolean alreadyBookmarked = (knowledge == Knowledge.KNOWN) ? true : false;when(bookmarkService.contains(anyString())).thenReturn(alreadyBookmarked);BlogAnalyzer blogAnalyzer = mock(BlogAnalyzer.class);boolean interesting = (quality == Quality.INTERESTING) ? true : false;when(blogAnalyzer.isInteresting(anyString())).thenReturn(interesting);AwesomenessFinder awesomenessFinder = new AwesomenessFinder(blogAnalyzer, bookmarkService);String url = "whatever";awesomenessFinder.checkBlog(url);if (expectedBookmark == ExpectedBookmark.STORED) {verify(bookmarkService).bookmark(url);} else {verify(bookmarkService, never()).bookmark(url);}if (expectedAnalysis == ExpectedAnalysis.ANALYZED) {verify(blogAnalyzer).isInteresting(url);} else {verify(blogAnalyzer, never()).isInteresting(url);}}
}
最后但并非最不重要的一点是,构造函数注入的一个不错的好处是能够将类的所有依赖项放在同一位置(构造函数)。 如果依赖项列表超出了控制范围,则构造函数的大小会产生非常明显的代码味道。 这表明您在班级中肯定承担了多个责任,您应该将其划分为多个班级,以便于进行单元测试更容易隔离。
翻译自: https://www.javacodegeeks.com/2013/04/spring-to-autowire-or-not-to-autowire.html