【问题标题】:I can't unit test my class without exposing private fields -- is there something wrong with my design?我不能在不暴露私有字段的情况下对我的课程进行单元测试——我的设计有问题吗?
【发布时间】:2013-10-11 05:25:21
【问题描述】:

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

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

以下是我的相关代码的粗略概述。应该有某种地址验证框架,可以通过不同的方式进行地址验证,例如通过一些外部网络服务或数据库中的数据或任何其他来源来验证它们。所以我有一个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,它使用一些外部网络服务进行验证,并像这样实现它:

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 将有点没用,只是将其所有工作委托给另一个组件,我会说:一层抽象太远了。

【问题讨论】:

  • 许多依赖注入框架使用反射来允许注入私有成员。有趣的是,Google Guava 包含一个注解 @VisibleForTesting
  • @EricJablow 很酷,我不知道这个注释,但它比注释要好得多,因为无论如何我已经在使用 Guava
  • @smajlo 谢谢,这当然是开始寻找答案的好地方。但我希望更多关于我的特定设计及其与单元测试/干净代码的关系的反馈。

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


【解决方案1】:

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

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

【讨论】:

  • 感谢您的建议阅读,我会很高兴地研究它。另外,关于 PowerMock 的优点。
【解决方案2】:

我同意 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) 原则:

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

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

【讨论】:

  • 谢谢,这是一个很好的答案,这表明即使没有任何框架,您也可以实现像 DI 一样的东西。
猜你喜欢
  • 1970-01-01
  • 2016-11-05
  • 2011-01-07
  • 1970-01-01
  • 2017-10-07
  • 1970-01-01
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多