我想知道如何单元测试抽象类,以及扩展抽象类的类。

我是否应该通过扩展抽象类来测试它,去掉抽象方法,然后测试所有的具体方法?然后只测试我重写的方法,并在单元测试中测试扩展抽象类的对象的抽象方法?

我是否应该有一个抽象测试用例,可以用来测试抽象类的方法,并在测试用例中为扩展抽象类的对象扩展该类?

注意,我的抽象类有一些具体的方法。


当前回答

遵循@patrick-desjardins的回答,我实现了抽象和它的实现类@Test,如下所示:

抽象类- ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

由于抽象类不能被实例化,但它们可以被子类化,具体类DEF.java如下:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

测试类来测试抽象方法和非抽象方法:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}

其他回答

这是我在设置一个测试抽象类的工具时通常遵循的模式:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

以及我在测试中使用的版本:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

如果在我不期望它的时候调用抽象方法,测试就会失败。在安排测试时,我可以很容易地用lambdas剔除抽象方法,这些方法执行断言、抛出异常、返回不同的值等。

遵循@patrick-desjardins的回答,我实现了抽象和它的实现类@Test,如下所示:

抽象类- ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

由于抽象类不能被实例化,但它们可以被子类化,具体类DEF.java如下:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

测试类来测试抽象方法和非抽象方法:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}

抽象基类有两种使用方式。

您正在专门化抽象对象,但所有客户端都将通过其基接口使用派生类。 您正在使用抽象基类来排除设计中对象中的重复,而客户端通过自己的接口使用具体实现。


1策略模式的解决方案

如果您有第一种情况,那么您实际上有一个由派生类正在实现的抽象类中的虚方法定义的接口。

您应该考虑使其成为一个真实的接口,将抽象类更改为具体类,并在其构造函数中获取该接口的实例。然后派生类就成为这个新接口的实现。

这意味着您现在可以使用新接口的模拟实例测试以前的抽象类,并通过现在的公共接口测试每个新实现。一切都是简单且可测试的。


解决方案2

如果您有第二种情况,那么您的抽象类将作为辅助类工作。

看看它包含的功能。看看是否可以将其中的任何一部分推到被操纵的对象上,以最小化复制。如果您还有剩余的东西,考虑将其作为一个helper类,您的具体实现使用它的构造函数并删除它们的基类。

这再次导致了简单且易于测试的具体类。


一般来说

喜欢简单对象的复杂网络,而不是复杂对象的简单网络。

可扩展可测试代码的关键是小的构建块和独立的连接。


更新:如何处理两者的混合?

有可能有一个基类同时扮演这两个角色……即:它有一个公共接口,并有受保护的helper方法。如果是这种情况,那么您可以将辅助方法分解为一个类(场景2),并将继承树转换为策略模式。

如果您发现一些方法是基类直接实现的,而另一些方法是虚的,那么您仍然可以将继承树转换为策略模式,但我也认为这是一个很好的指标,说明职责没有正确对齐,可能需要重构。


更新2:抽象类作为垫脚石(2014/06/12)

前几天我遇到了一个使用抽象的情况,所以我想探讨一下为什么。

我们的配置文件有一个标准格式。这个特殊的工具有3个配置文件都是这种格式。我希望每个设置文件都有一个强类型类,这样通过依赖注入,类就可以请求它所关心的设置。

我通过一个抽象基类来实现这一点,它知道如何解析设置文件格式和派生类,这些派生类公开了这些相同的方法,但封装了设置文件的位置。

我本可以编写一个由3个类包装的“SettingsFileParser”,然后委托给基类以公开数据访问方法。我选择不这样做,因为这将导致3个派生类,其中的委托代码比其他任何类都多。

However... as this code evolves and the consumers of each of these settings classes become clearer. Each settings users will ask for some settings and transform them in some way (as settings are text they may wrap them in objects of convert them to numbers etc.). As this happens I will start to extract this logic into data manipulation methods and push them back onto the strongly typed settings classes. This will lead to a higher level interface for each set of settings, that is eventually no longer aware it's dealing with 'settings'.

此时,强类型设置类将不再需要公开底层“设置”实现的“getter”方法。

在这一点上,我将不再希望他们的公共接口包括设置访问器方法;所以我将改变这个类封装一个设置解析器类,而不是从它派生。

因此,对于我来说,抽象类是一种暂时避免委托代码的方法,也是代码中的一个标记,用来提醒我稍后更改设计。我可能永远也见不到它了,所以它可能会活很久……只有密码能告诉我们。

我发现这适用于任何规则……比如“没有静态方法”或者“没有私有方法”。它们表明代码中有一种气味……这很好。它让你不断寻找你错过的抽象概念……同时让你继续为你的客户提供价值。

我想象像这样的规则定义了一个景观,可维护的代码生活在山谷中。当你添加新的行为时,它就像雨水降落在你的代码上。一开始你把它放在它落地的地方。然后你重构,让优秀设计的力量推动行为,直到一切都在山谷中结束。

首先,如果抽象类包含一些具体的方法,我认为你应该这样做,考虑这个例子

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

我反对“抽象”测试。我认为测试是一个具体的概念,没有抽象的概念。如果您有公共元素,请将它们放在辅助方法或类中供所有人使用。

至于测试抽象测试类,一定要问自己要测试的是什么。有几种方法,您应该找出适合您的场景的方法。您是否试图在子类中测试一个新方法?然后让您的测试只与该方法交互。测试基类中的方法吗?然后可能只针对该类有一个单独的fixture,并使用尽可能多的测试单独测试每个方法。