我不能在没有暴露私人领域的情况下对我的班级进行单元测试 - 我的设计有问题吗?

时间:2013-10-11 05:25:21

标签: java oop unit-testing dependency-injection coding-style

我已经编写了一些我认为设计得很好的代码,但后来我开始为它编写单元测试并停止这么肯定。

事实证明,为了编写一些合理的单元测试,我需要将我的一些变量访问修饰符从private更改为default,即暴露它们(仅在包内,但仍然...)。

以下是我的代码的粗略概述。应该有某种地址验证框架,它可以通过不同方式进行地址验证,例如:通过某些外部Web服务或DB中的数据或任何其他来源验证它们。所以我有Module的概念,就是这样:一种验证地址的独立方式。我有一个界面:

interface Module {

  public void init(InitParams params);

  public ValidationResponse validate(Address address);
}

有某种工厂,基于请求或会话状态选择适当的模块:

class ModuleFactory {

  Module selectModule(HttpRequest request) {
       Module module = chooseModule(request);// analyze request and choose a module
       module.init(createInitParams(request)); // init module
       return module;
  }

}

然后,我编写了一个Module,它使用一些外部Web服务进行验证,并按照以下方式实现:

WebServiceModule {
   private WebServiceFacade webservice;     

   public void init(InitParams params) {
      webservice = new WebServiceFacade(createParamsForFacade(params));
   }

   public ValidationResponse validate(Address address) {
      WebService wsResponse = webservice.validate(address);
      ValidationResponse reponse = proccessWsResponse(wsResponse);
      return response;
   }

}

所以基本上我有这个WebServiceFacade,它是外部Web服务的包装器,我的模块调用这个外观,处理它的响应并返回一些框架标准的响应。

我想测试WebServiceModule是否正确处理来自外部Web服务的响应。显然,我不能在单元测试中调用真正的Web服务,所以我在嘲笑它。但是,为了让模块使用我的模拟Web服务,必须可以从外部访问字段webservice。它打破了我的设计,我想知道我能做些什么。显然,Facade不能在init参数中传递,因为ModuleFactory不会也不应该知道它是必需的。

我已经读过依赖注入可能是这些问题的答案,但我看不出怎么样?我以前没有使用任何DI框架,比如Guice,所以我不知道它是否可以在这种情况下轻松使用。但也许可以吗?

或许我应该改变我的设计?

或者搞砸它并让这个不幸的现场包私有(但留下像// default visibility to allow testing (oh well...)这样的悲伤评论并不感觉正确吗?

呸!在我写这篇文章的时候,我发现我可以创建一个WebServiceProcessor,它将WebServiceFacade作为构造函数参数,然后只测试WebServiceProcessor。这将是我的问题的解决方案之一。你怎么看待这件事?我有一个问题,因为那时我的WebServiceModule会有点无用,只是把它的所有工作委托给另一个组件,我会说:一层抽象太过分了。

2 个答案:

答案 0 :(得分:2)

是的,你的设计错了。你应该在你的班级里面做dependency injection而不是new ...(这也称为“硬编码依赖”)。无法轻松编写测试是错误设计的完美指标(请阅读Growing Object-Oriented Software Guided by Tests中的“倾听您的测试”范例。)

BTW,在这种情况下使用像PowerMock这样的反射或依赖性破坏框架是一种非常糟糕的做法,应该是你最后的选择。

答案 1 :(得分:1)

我同意yegor256所说的内容,并且我想建议你最终遇到这种情况的原因是你已经为你的模块分配了多个职责:创建和验证。这与Single responsibility principle相反,并且有效地限制了您与验证分开测试创建的能力。

考虑将“模块”的责任限制在单独创建中。当他们只有这个责任时,命名也可以改进:

interface ValidatorFactory {
  public Validator createValidator(InitParams params);
}

验证界面变得分开:

interface Validator {
  public ValidationResponse validate(Address address); 
}

然后您可以从实施工厂开始:

class WebServiceValidatorFactory implements ValidatorFactory {
  public Validator createValidator(InitParams params) {
    return new WebServiceValidator(new ProdWebServiceFacade(createParamsForFacade(params)));
  }
}

这个工厂代码变得难以进行单元测试,因为它明确地引用了prod代码,所以请保持这个impl非常简洁。将任何逻辑(如createParamsForFacade)放在一边,以便您可以单独测试它。

Web服务验证程序本身只负责验证,并按照Inversion of Control (IoC)原则将façade作为依赖项:

class WebServiceValidator implements Validator {
  private final WebServiceFacade facade;

  public WebServiceValidator(WebServiceFacade facade) {
    this.facade = facade;
  }

  public ValidationResponse validate(Address address) {
    WebService wsResponse = webservice.validate(address);
    ValidationResponse reponse = proccessWsResponse(wsResponse);
    return response;
  }
}

由于WebServiceValidator不再控制其依赖项的创建,因此测试变得轻而易举:

@Test
public void aTest() {
   WebServiceValidator validator = new WebServiceValidator(new MockWebServiceFacade());
   ...
}

这样你就有效地颠倒了对依赖关系创建的控制:控制反转(IoC)!

哦,顺便说一句,先写下你的测试。这样,您自然会倾向于可测试的解决方案,这通常也是最好的设计。我认为这是因为测试需要模块化,而模块化恰好是良好设计的标志。