You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 07:45:38