我正在阅读有关单元测试,模拟和所有这些内容的很多内容。我目前也正在阅读这本书"以测试为导向的面向对象软件的增长#34;史蒂夫弗里曼和纳特普赖斯。
我开始理解很多东西,但遗漏了一个关键点,我试图在网上找到答案,但我还不满意。
在下面的示例中,我有一个在线商店,它接收来自第三方库的消息,翻译它们,解释它们并最终在需要时将它们保存到数据库中。在具体案例中,我收到一条消息,指出用户信用卡地址的变更,并希望将该信息存储到数据库中。
结构如下:
src/
domain/
MessageTranslator.java
ShopEventListener.java
ShopHandler.java
model/
CreditCard.java
CreditCardBase.java
CreditCardBuilder.java
User.java
UserBase.java
UserBuilder.java
test/
MessageTranslatorTest.java
ShopHandlerTest.java
MessageTranslatorTest
public class MessageTranslatorTest {
@Test
public void notifiesCCAddressChangedWhenChangeCCAddressMessageReceived() throws Exception {
ShopEventListener listenerMock = mock(ShopEventListener.class);
MessageTranslator messageTranslator = new MessageTranslator(listenerMock);
messageTranslator.processMessage("action=changeCCAddress; firstname=John; lastname=Doe; address=foobar3");
verify(listenerMock).ccAddressChanged("John", "Doe", "foobar3");
}
}
MessageTranslator (现在非常简单)
public class MessageTranslator {
private final ShopEventListener listener;
public MessageTranslator(ShopEventListener userEventListener) {
listener = userEventListener;
}
public void processMessage(String message) throws Exception {
String[] attributes = message.split(";");
listener.ccAddressChanged(attributes[1].split("=")[1].trim(), attributes[2].split("=")[1].trim(), attributes[3].split("=")[1].trim());
}
}
ShopHandler
public class ShopHandler implements ShopEventListener {
@Override
public void ccAddressChanged(String firstname, String lastname, String newAddress) throws Exception {
// find a user (especially userid) in the Database for given firstname and lastname
UserBase userBase = new UserBase();
User user = userBase.find(aUser().withFirstname(firstname).withLastname(lastname).build());
if (user == null) {
throw new Exception();
}
// find the matching CreditCard for the userid in the database
Integer userid = user.getUserid();
CreditCardBase ccBase = new CreditCardBase();
CreditCard cc = ccBase.find(aCreditCard().withUserid(userid).build());
if (cc == null) {
throw new Exception();
}
// change address locally and then write it back to the database
cc.setAddress(newAddress);
cc.persist();
}
}
ShopHandlerTest
public class ShopHandlerTest {
@Test
public void changesCCAddressWhenChangeCCAddressEventReceived() throws Exception {
ShopHandler shop = new ShopHandler();
shop.ccAddressChanged("John", "Doe", "foobar3");
// TODO: How to test the changes in inner object?
}
}
这是我总是绊倒的地方。
对于这篇长篇文章感到抱歉,如果你能把我推向正确的方向,真的很棒。 如果您需要更多代码来理解上述内容,请与我们联系。
答案 0 :(得分:1)
我是否要模拟帮助程序类UserBase和CreditCardBase不执行任何数据库查询但只返回准备好的假对象?
看起来你的“助手类”实际上是存储库/ DAO。您通常希望与DAO分开测试业务逻辑,而无需真正的数据库访问。所以是的,您应该模拟这些DAO并准备对它们的调用,因为它们将实际工作。在大多数情况下,准备好的假物体都可以。您可能还想验证您的模拟DAO是否实际被调用。
我是否想要模拟persist-method以不将任何实际数据写入数据库,但是可能只是测试要持久化的对象的参数并让其他(集成)测试测试数据库操作?
我发现您的业务实体中似乎有persist
方法,这有点奇怪。通常DAO实现这种方法。
是的,如果您测试业务逻辑,那么您也应该模拟对{DAO}的persist
调用。如果你不这样做,那么你将对业务逻辑进行更加严格的测试。
是的,你应该测试你的DAO,但要与业务逻辑分开。
如果1.和2.将回答是,那么我在这里测试的是什么?这个单位值得单元测试吗?
您正在测试业务逻辑。正是您在ccAddressChanged
方法中实施的内容。大致是:
这种结构是否有意义?
这不是我习惯的。你似乎在实体中有数据访问逻辑,那么你也有这个“基础”助手classess ......
如果1.和2.将回答是,那么我该如何模拟内部对象?
对于“内部对象”,你可能意味着这些辅助类。它们实际上更像是“辅助类”,它们是提供数据库访问权限的DAO。你可以从外面传递或注射它们。基本上这是依赖注入,您的业务逻辑依赖于这些DAO组件。如果您能够从外部传递它们,那么在测试中您可以模拟DAO并将模拟传递给您的业务服务。使用像Spring这样的DI框架,你可以获得框架支持。
这里是一个粗略的草图,展示了如何使用Spring和Mockito对ShopHandler
类进行测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ShopHandler.class})
public class ShopHandlerTest {
@Autowired
private ShopHandler sut;
@MockBean
private UserRepository userRepository;
@MockBean
private CreditCardRepository creditCardRepository;
@Test(expected = UserNotFoundException.class)
public void throwsUserNotFoundExceptionIfUserIsUnknown() {
when(userRepository.findUserByFirstNameAndLastName("Scott", "Tiger").thenReturn(null);
sut.ccAddressChanged("Scott", "Tiger", "Some Address");
}
@Test
public void successFullyUpdatesCreditCardAddress() {
when(userRepository.findUserByFirstNameAndLastName("Scott", "Tiger").thenReturn(new User("userId", ...));
when(creditCardRepository.findByUserId("userId")).thenReturn(new CreditCard(...));
ArgumentCaptor<CreditCard> creditCardCaptor = ArgumentCaptor.forClass(CreditCard.class);
verify(creditCardRepository).save(creditCardCaptor.capture());
sut.ccAddressChanged("Scott", "Tiger", "Some Address");
asserthThat(creditCardCaptor.getValue().getAddress()).isEqualTo("Some Address");
}
}
我觉得依赖注入就是这里的方法,
依赖注入是一种非常明智的方法。
因为首先它没有真正的依赖,
嗯,当然这些都是真正的依赖。
但是有些辅助类,
你认为它最终会成为“帮助者类”并开始成为“真正的依赖”?你所谓的“辅助类”非常类似于绝对是“真正依赖”的DAO。
第二个(也就是更重要的是)ShopHandler类可能充斥着依赖关系,因为它可能需要很多不同的辅助类和模型类来执行所有不同的操作。
如果您需要执行所有这些操作并需要所有这些依赖项来执行此操作,那么这就是现实。但问题是 - 你真的必须在一个商业服务中实施所有这些行动吗?你不能把它分成许多商业服务吗?然后你会得到更小的更集中的类,并且它们只需要一些依赖。
答案 1 :(得分:0)
由于您在方法UserBase
中使用CreditCard
关键字创建new
和ccAddressChanged()
个实例,因此您无法嘲笑它们!
为了能够模拟它们,可以通过将这些类的实例注入ccAddressChanged()
来使用DI - Dependency Injection(也称为IoC - Inversion Of Control):
更改类的签名:
public void ccAddressChanged(String firstname, String lastname, String newAddress)
为:
public void ccAddressChanged(String firstname, String lastname, String newAddress, UserBase userBase, CreditCard creditCard)
这样,您就可以模拟它们(使用Mockito或任何其他模拟框架)并将模拟发送到方法。
使用Mockito进行测试的示例:
@Test
public void changesCCAddressWhenChangeCCAddressEventReceived() throws Exception {
ShopHandler shop = new ShopHandler();
// mock UserBase and its behavior
UserBase mockedUserBase = mock(UserBase.class)
when(mockedUserBase.find(any()).thenReturns(mock(User.class));
// mock CreditCard
CreditCard mockedCreditCard = mock(CreditCard.class);
shop.ccAddressChanged("John", "Doe", "foobar3");
}
我觉得依赖注入是错误的方法,因为 首先它没有真正的依赖,但是一些辅助类,第二个(和 更重要的是,ShopHandler类可能充斥着 依赖
DI 不错误:
ShopHandler
类确实真正依赖UserBase
和CreditCardBase
ShopHandler
的构造函数并将它们保存到私有字段中。这种方式在初始化期间只执行一次,不会给用户带来负担,也不会暴露实现细节。 此外,假设您重构了代码,现在您在构造函数中分配UserBase
和CreditCardBase
。我会重构代码:
@Override
public void ccAddressChanged(String firstname, String lastname, String newAddress) throws Exception {
// find a user (especially userid) in the Database for given firstname and lastname
UserBase userBase = new UserBase();
User user = userBase.find(aUser().withFirstname(firstname).withLastname(lastname).build());
if (user == null) {
throw new Exception();
}
// find the matching CreditCard for the userid in the database
Integer userid = user.getUserid();
CreditCardBase ccBase = new CreditCardBase();
CreditCard cc = ccBase.find(aCreditCard().withUserid(userid).build());
if (cc == null) {
throw new Exception();
}
// change address locally and then write it back to the database
cc.setAddress(newAddress);
cc.persist();
}
为:
@Override
public void ccAddressChanged(String firstname, String lastname, String newAddress) throws Exception {
User user = getUserByName(firstname, lastname);
CreditCard creditCard = getCCByUser(user);
setAddress(creditCard, newAddress);
}
现在您不必再对此ccAddressChanged()
进行单元测试了。您应该做的是测试,以及三种方法中的每一种:getUserByName
,getCCByUser
和setAddress
。而且每一个都很容易模拟和测试!
答案 2 :(得分:0)
以下是我为ShopHandler
编写集成测试的方法(如问题所示,没有任何更改):
public class ShopHandlerTest {
@Tested(fullyUnitialized = true) AppDB appDB;
@Tested ShopHandler sut;
@Test(expected = UserNotFoundException.class)
public void throwsUserNotFoundExceptionIfUserIsUnknown() {
sut.ccAddressChanged("Unknown", "user", "...");
}
@Test
public void successFullyUpdatesCreditCardAddress() {
User user = new User("Scott", "Tiger");
appDB.persist(user);
CreditCard cc = new CreditCard(user, ...);
appDB.persist(cc);
String newAddress = "New address";
sut.ccAddressChanged(user.getFirstName(), user.getLastName(), newAddress);
appDB.refresh(cc);
assertEquals(newAddress, cc.getAddress());
}
}
上面,@Tested
是一个JMockit注释,支持完全DI,还有JPA / EJB / etc。支持。它可以用作元注释,因此您可以创建@SUT
或@TestUtil
注释以简化其在测试中的使用。
完全可重用的测试实用程序类AppDB
将是这样的:
public final class AppDB {
@PersistenceContext private EntityManager em;
@PostConstruct
private void startTransaction() { ... using em... }
@PreDestroy
private void endTransaction() { ... rollback using em... }
public void persist(Object entity) { em.persist(entity); }
public void refresh(Object entity) { em.refresh(entity); }
}
请注意那些集成测试看起来多么简单。它们基本上只包含高级代码,基本上是您在生产(SUT)代码中看到的相同类型的代码。没有复杂的模拟API可以帮助您解决问题。它们也快速而稳定。