0

我已经编写了一些我认为设计得非常好的代码,但后来我开始为它编写单元测试并且不再那么确定了。

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

这是我有问题的代码的一些粗略概述。应该有某种地址验证框架,可以通过不同的方式进行地址验证,例如通过某些外部 Web 服务或通过数据库中的数据或任何其他来源来验证它们。所以我有一个概念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将 aWebServiceFacade作为构造函数参数的,然后只测试WebServiceProcessor. 这将是我的问题的解决方案之一。你怎么看待这件事?我对此有一个问题,因为那样我WebServiceModule会有点无用,只是将其所有工作委托给另一个组件,我会说:一层抽象太远了。

4

2 回答 2

2

是的,你的设计是错误的。您应该进行依赖注入而不是new ...在您的类中(也称为“硬编码依赖”)。无法轻松编写测试是错误设计的完美指标(请阅读“测试指导的面向对象软件的成长”中的“聆听您的测试”范例)。

顺便说一句,在这种情况下,使用像PowerMock这样的反射或依赖破坏框架是一种非常糟糕的做法,应该是你最后的手段。

于 2013-10-11T10:41:20.967 回答
1

我同意 yegor256 的说法,并想建议您最终陷入这种情况的原因是您为模块分配了多个职责:创建和验证。这违背了单一职责原则,并有效地限制了您将创建与验证分开进行测试的能力。

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

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 服务验证器本身只承担验证的责任,并以外观为依赖项,遵循控制反转 (IoC)原则:

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)!

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

于 2013-10-11T14:49:31.110 回答