ChatGPT解决这个技术问题 Extra ChatGPT

如何对抽象类进行单元测试:用存根扩展?

我想知道如何对抽象类和扩展抽象类的类进行单元测试。

我是否应该通过扩展抽象类、存根抽象方法来测试抽象类,然后测试所有具体方法?然后只测试我覆盖的方法,并在单元测试中测试扩展我的抽象类的对象的抽象方法?

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

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

最好不要直接对抽象类进行单元测试:enterprisecraftsmanship.com/posts/…
如果您有 20 或 30 个方法在所有子类中具有完全相同的实现怎么办?您仍然应该在所有这些中重复测试吗?这没有多大意义

a
awesoon

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

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

解决方案 1 - 策略模式

https://i.stack.imgur.com/Bu4Vy.png

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

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

https://i.stack.imgur.com/wPPqA.png

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

解决方案 2

如果您有第二种情况,那么您的抽象类正在充当帮助类。

https://i.stack.imgur.com/ABrGO.png

看看它包含的功能。看看是否可以将其中的任何一个推到正在被操纵的对象上以最小化这种重复。如果您还有任何剩余,请考虑使其成为您的具体实现在其构造函数中采用的辅助类并删除它们的基类。

https://i.stack.imgur.com/eySjQ.png

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

作为一项规则

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

可扩展可测试代码的关键是小型构建块和独立布线。

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

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

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

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

前几天我遇到了一个使用abstract的情况,所以我想探讨一下原因。

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

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

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

但是...随着此代码的发展以及这些设置类中的每一个的使用者变得更加清晰。每个设置用户都会要求一些设置并以某种方式对其进行转换(因为设置是文本,他们可以将它们包装在将它们转换为数字等的对象中)。发生这种情况时,我将开始将此逻辑提取到数据操作方法中,并将它们推回到强类型设置类中。这将为每组设置带来更高级别的界面,最终不再意识到它正在处理“设置”。

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

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

因此,Abstract 类是:一种让我暂时避免使用委托代码的方法,以及在代码中提醒我稍后更改设计的标记。我可能永远也达不到它,所以它可能会活很长一段时间……只有代码可以说明。

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

我想象像这样的规则定义了一个景观,可维护的代码存在于山谷中。当您添加新行为时,就像雨点落在您的代码上一样。最初你把它放在它所在的任何地方......然后你重构以允许良好设计的力量推动行为,直到它全部结束在山谷中。


这是一个很好的答案。比评分最高的好多了。但是我想只有那些真正想编写可测试代码的人才会喜欢它.. :)
我无法理解这实际上是一个多么好的答案。它完全改变了我对抽象类的思考方式。谢谢奈杰尔。
哦不..另一个我必须重新考虑的原则!谢谢(现在既讽刺又非讽刺,一旦我吸收了它并感觉自己是一个更好的程序员)
不错的答案。绝对是要考虑的事情......但你所说的基本上不是归结为不使用抽象类吗?
+1 仅适用于规则,“在复杂对象的简单网络中更喜欢由简单对象组成的复杂网络。”
m
mezoid

编写一个 Mock 对象并将它们仅用于测试。它们通常非常非常非常小(从抽象类继承)而不是更多。然后,在您的单元测试中,您可以调用您想要测试的抽象方法。

您应该测试包含一些逻辑的抽象类,就像您拥有的所有其他类一样。


该死,我不得不说这是我第一次同意使用模拟的想法。
Daok:你能举个简单的例子吗?我不知道,为什么我应该使用 Mock 而不是最小类。还是我误会你了?提前致谢!
你需要两个类,一个模拟和测试。模拟类仅扩展被测抽象类的抽象方法。这些方法可以是无操作、返回 null 等,因为它们不会被测试。测试类只测试非抽象的公共API(即抽象类实现的接口)。对于任何扩展 Abstract 类的类,您都需要额外的测试类,因为没有涵盖抽象方法。
显然可以这样做..但是要真正测试任何派生类,您将一遍又一遍地测试此基本功能..这会导致您拥有一个抽象测试夹具,因此您可以在测试中排除这种重复。这都臭了!我强烈建议再看看你为什么首先使用抽象类,看看其他方法是否会更好。
@liltitus27 有很多方法可以编写编译器理解的代码。作为经验丰富的编码人员,您已经过滤了一组可能的设计,以丢弃具有高耦合或令人困惑或不可维护的设计。通过你学习的过滤器的设计被归类为“好设计”。我还有一个判断设计的标准。我可以轻松测试吗?
M
Mnementh

我为抽象类和接口做的事情如下:我编写了一个测试,它使用具体的对象。但是测试中没有设置X类型的变量(X是抽象类)。这个测试类没有添加到测试套件中,而是它的子类,它们有一个设置方法,将变量设置为 X 的具体实现。这样我就不会复制测试代码。如果需要,未使用测试的子类可以添加更多测试方法。


这不会导致子类中的转换问题吗?如果 X 有方法 a 并且 Y 继承了 X 但也有方法 b。当您子类化您的测试类时,您是否不必将抽象变量转换为 Y 才能对 b 执行测试?
当您的测试测试位于基类中的逻辑时,这会重复测试工作。
佚名

要专门对抽象类进行单元测试,您应该派生它以用于测试目的、测试 base.method() 结果和继承时的预期行为。

你通过调用一个方法来测试它,所以通过实现它来测试一个抽象类......


S
Seth Petry-Johnson

如果您的抽象类包含具有商业价值的具体功能,那么我通常会通过创建一个存根抽象数据的测试替身来直接对其进行测试,或者使用模拟框架为我执行此操作。我选择哪一个很大程度上取决于我是否需要编写抽象方法的特定于测试的实现。

我需要执行此操作的最常见情况是当我使用 Template Method pattern 时,例如当我构建将由第 3 方使用的某种可扩展框架时。在这种情况下,抽象类定义了我要测试的算法,因此测试抽象基础比测试特定实现更有意义。

但是,我认为这些测试应该只关注真实业务逻辑的具体实现,这一点很重要。你不应该对抽象类的实现细节进行单元测试,因为你最终会得到脆弱的测试。


R
Ray Tayek

一种方法是编写一个与您的抽象类相对应的抽象测试用例,然后编写子类化您的抽象测试用例的具体测试用例。对原始抽象类的每个具体子类执行此操作(即,您的测试用例层次结构反映了您的类层次结构)。请参阅 junit recipies 手册中的测试接口:http://safari.informit.com/9781932394238/ch02lev1sec6https://www.manning.com/books/junit-recipeshttps://www.amazon.com/JUnit-Recipes-Practical-Methods-Programmer/dp/1932394230(如果您没有 Safari 帐户)。

另请参阅 xUnit 模式中的测试用例超类:http://xunitpatterns.com/Testcase%20Superclass.html


c
casademora

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

至于测试一个抽象测试类,请确保你问自己你正在测试什么。有几种方法,您应该找出在您的场景中有效的方法。您是否正在尝试在您的子类中测试一种新方法?然后让您的测试仅与该方法交互。您是否正在测试基类中的方法?然后可能只为该类有一个单独的夹具,并根据需要使用尽可能多的测试单独测试每个方法。


我不想重新测试我已经测试过的代码,这就是我走抽象测试用例道路的原因。我试图在一个地方测试我的抽象类中的所有具体方法。
我不同意将通用元素提取到辅助类中,至少在某些(很多?)情况下是这样。如果一个抽象类包含一些具体的功能,我认为直接对该功能进行单元测试是完全可以接受的。
如果您有数十种具体实现的方法,那么只需将其复制粘贴到您的所有具体实现中?这没有任何意义
佚名

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

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 来存根抽象方法,这些方法执行断言、抛出异常、返回不同的值等。


J
Jeb

如果具体方法调用了该策略不起作用的任何抽象方法,并且您希望分别测试每个子类的行为。否则,扩展它并像您描述的那样对抽象方法进行存根应该没问题,再次提供抽象类的具体方法与子类分离。


A
Ace

我想你可能想测试一个抽象类的基本功能......但你可能最好通过扩展类而不覆盖任何方法,并对抽象方法进行最小的模拟。


b
bryanbcook

使用抽象类的主要动机之一是在应用程序中启用多态性——即:您可以在运行时替换不同的版本。实际上,这与使用接口非常相似,只是抽象类提供了一些通用管道,通常称为模板模式。

从单元测试的角度来看,有两件事需要考虑:

您的抽象类与其相关类的交互。使用模拟测试框架非常适合这种情况,因为它表明您的抽象类与其他类配合得很好。派生类的功能。如果您有为派生类编写的自定义逻辑,则应该单独测试这些类。

编辑:RhinoMocks 是一个很棒的模拟测试框架,它可以在运行时通过从你的类中动态派生来生成模拟对象。这种方法可以为您节省无数小时的手动编写派生类的时间。


s
shreeram banne

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

 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.

       }

   }

b
banduki

如果抽象类适合您的实现,请测试(如上所述)派生的具体类。你的假设是正确的。

为避免将来混淆,请注意这个具体的测试类不是模拟的,而是假的。

严格来说,模拟由以下特征定义:

使用模拟来代替正在测试的主题类的每个依赖项。

模拟是接口的伪实现(您可能还记得,作为一般规则,依赖项应该声明为接口;可测试性是这样做的主要原因之一)

模拟接口成员的行为——无论是方法还是属性——在测试时提供(同样,通过使用模拟框架)。这样,您可以避免将正在测试的实现与其依赖项的实现耦合(它们都应该有自己的离散测试)。


u
user2340612

在@patrick-desjardins 回答之后,我实现了 abstract 及其实现类以及 @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!";
    }
}

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

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"));
    }
}