Mockito测试中自定义返回对象的实现方案优化咨询
单元测试中构造属性缺失的Document对象方案探讨
我正在编写若干单元测试,部分用于测试Document类的属性缺失场景。使用when(repository.findOne(id)).thenReturn(DOCUMENT_WITHOUT_SOMETHING)时,这些DOCUMENT_WITHOUT_SOMETHING是根据测试用例预定义属性的final对象。例如,当加载无name属性的Document时,方法会返回null并输出日志。我创建了测试数据类DocumentServiceTestData来声明所需对象和属性,请问这种方案是否合理?有没有无需新建类的更清晰实现方式?
以下是简化代码:
Document类
public class Document { private String name; private String applicant; private String sign; private boolean signed; public boolean isSigned() {return signed;} public String getApplicant() {return applicant;} public String getName() {return name;} public String getSign() {return sign;} public void setSigned(boolean signed) {this.signed = signed;} public void setApplicant(String applicant) {this.applicant = applicant;} public void setName(String name) {this.name = name;} public void setSign(String sign) {this.sign = sign;} }
DocumentService类
@Service public class DocumentService { private static final Logger LOG = LoggerFactory.getLogger(DocumentService.class); private DocumentRepository documentRepository; public Document loadDocument(Long docId) { Document document = documentRepository.findByIdOrFail(docId); if(document.getName() == null) { LOG.info("There is no name for document, returning null"); return null; } if(document.getSign() == null) { LOG.info("The document is not signed, returning null"); return null; } return document; } }
现有测试数据类DocumentServiceTestData
public class DocumentServiceTestData { public static final Long DOC_ID = 1L; public static final String APPLICANT = "Applicant"; public static final String DOC_NAME = "Document"; public static final String SIGN = "Sign"; public static final Document DOCUMENT_WITHOUT_NAME; public static final Document DOCUMENT_WITHOUT_SIGN; static { DOCUMENT_WITHOUT_NAME = makeDocument(null, APPLICANT, SIGN, true); DOCUMENT_WITHOUT_SIGN = makeDocument(DOC_NAME, APPLICANT, null, false); } private static Document makeDocument(String name, String applicant, String sign, boolean signed) { Document document = new Document(); document.setName(name); document.setApplicant(applicant); document.setSign(sign); document.setSigned(signed); return document; } }
测试类DocumentServiceUnitTest
@RunWith(MockitoJUnitRunner.class) @Category(MockTest.class) public class DocumentServiceUnitTest { @InjectMocks private DocumentService service; @Mock private DocumentRepository documentRepository; @Test public void testLoadDocument_notSigned(){ when(documentRepository.findByIdOrFail(DOC_ID)).thenReturn(DOCUMENT_WITHOUT_SIGN); Document document = service.loadDocument(DOC_ID); Assert.assertNull(document); } @Test public void testLoadDocument_notNamed(){ when(documentRepository.findByIdOrFail(DOC_ID)).thenReturn(DOCUMENT_WITHOUT_NAME); Document document = service.loadDocument(DOC_ID); Assert.assertNull(document); } }
现有方案的合理性分析
你当前的方案是完全合理的,优势如下:
- 测试数据集中管理:将所有测试用的常量和预定义对象放在单独类中,避免在多个测试用例里重复构造相同对象,减少冗余代码。
- 可读性与维护性高:常量命名(如
DOCUMENT_WITHOUT_NAME)清晰直观,测试用例的意图一目了然;工厂方法makeDocument把对象构造逻辑集中,后续修改属性规则只需调整这一处。 - 复用性强:如果后续新增测试用例需要相同的属性缺失对象,可以直接复用已定义的常量,无需重复编写构造代码。
无需新建类的替代实现方式
如果不想单独创建测试数据类,以下几种方式可以实现更简洁的测试对象构造:
1. 将测试数据逻辑内嵌到测试类中
把DocumentServiceTestData里的静态常量和工厂方法直接移到DocumentServiceUnitTest内部作为静态成员,既保留了集中管理的优势,又不用额外创建类:
@RunWith(MockitoJUnitRunner.class) @Category(MockTest.class) public class DocumentServiceUnitTest { @InjectMocks private DocumentService service; @Mock private DocumentRepository documentRepository; // 内嵌测试常量与工厂方法 private static final Long DOC_ID = 1L; private static final String APPLICANT = "Applicant"; private static final String DOC_NAME = "Document"; private static final String SIGN = "Sign"; private static final Document DOCUMENT_WITHOUT_NAME; private static final Document DOCUMENT_WITHOUT_SIGN; static { DOCUMENT_WITHOUT_NAME = makeDocument(null, APPLICANT, SIGN, true); DOCUMENT_WITHOUT_SIGN = makeDocument(DOC_NAME, APPLICANT, null, false); } private static Document makeDocument(String name, String applicant, String sign, boolean signed) { Document document = new Document(); document.setName(name); document.setApplicant(applicant); document.setSign(sign); document.setSigned(signed); return document; } // 测试方法... }
2. 使用Builder模式构造对象(需修改Document类)
给Document添加Builder内部类,这样可以在测试用例中灵活构造指定属性的对象,无需预定义常量:
// 修改后的Document类 public class Document { // 原有属性与getter/setter不变 // 添加Builder public static class Builder { private String name; private String applicant; private String sign; private boolean signed; public Builder applicant(String applicant) { this.applicant = applicant; return this; } public Builder name(String name) { this.name = name; return this; } public Builder sign(String sign) { this.sign = sign; return this; } public Builder signed(boolean signed) { this.signed = signed; return this; } public Document build() { Document doc = new Document(); doc.setName(name); doc.setApplicant(applicant); doc.setSign(sign); doc.setSigned(signed); return doc; } } } // 测试用例中使用Builder构造对象 @Test public void testLoadDocument_notSigned(){ Document docWithoutSign = new Document.Builder() .applicant("Applicant") .name("Document") .sign(null) .signed(false) .build(); when(documentRepository.findByIdOrFail(1L)).thenReturn(docWithoutSign); Assert.assertNull(service.loadDocument(1L)); }
3. 测试方法内直接构造对象
如果每个测试用例的对象差异较大,无需复用,也可以直接在测试方法内构造所需对象,逻辑直观:
@Test public void testLoadDocument_notNamed(){ Document docWithoutName = new Document(); docWithoutName.setApplicant("Applicant"); docWithoutName.setSign("Sign"); docWithoutName.setSigned(true); // 不设置name,保持为null when(documentRepository.findByIdOrFail(1L)).thenReturn(docWithoutName); Assert.assertNull(service.loadDocument(1L)); }
4. 使用Mockito Spy对象(适合简单场景)
如果不想手动设置多个属性,可以用Mockito的Spy对象来模拟指定属性的返回值:
@Test public void testLoadDocument_notSigned(){ Document doc = spy(new Document()); when(doc.getSign()).thenReturn(null); when(doc.getName()).thenReturn("Document"); when(doc.getApplicant()).thenReturn("Applicant"); when(doc.isSigned()).thenReturn(false); when(documentRepository.findByIdOrFail(1L)).thenReturn(doc); Assert.assertNull(service.loadDocument(1L)); }
这种方式适合仅需修改少数属性的场景,但若需要设置多个属性,不如直接构造对象清晰。
场景选择建议
- 若测试用例多、需要频繁复用相同属性缺失的对象,单独的测试数据类或内嵌到测试类的静态成员都是最优选择;
- 若测试用例对象差异大、复用率低,直接在方法内构造或使用Builder模式会更灵活;
- 若无法修改
Document类,且仅需模拟少数属性,Spy对象可以作为补充方案。
内容的提问来源于stack exchange,提问作者TomMathee
相关产品推荐
相关产品推荐

