如何测试(java)fluent API?

如何测试(java)fluent API?,java,unit-testing,junit,junit4,fluent,Java,Unit Testing,Junit,Junit4,Fluent,我正在构建一个像这样的java fluent API new Job().doFirst().doAfterFirst().thisOne(); new Job().doFirst().doAfterFirst().orThisOne(); 我可以通过定义一些类/接口来实现,比如 public class Job { DoAfterFirst doFirst(); } public interface DoAfterFirst { FinishWith doAfterFirs

我正在构建一个像这样的java fluent API

new Job().doFirst().doAfterFirst().thisOne();
new Job().doFirst().doAfterFirst().orThisOne();
我可以通过定义一些类/接口来实现,比如

public class Job {
    DoAfterFirst doFirst();
}

public interface DoAfterFirst {
    FinishWith doAfterFirst();
}

public interface FinishWith {
    void thisOne();
    void orThisOne();
}
因为它是DSL而不是
生成器
,所以我想测试方法的顺序。例如,我想测试一下,如果我调用了
doAfterFirst()
,我现在只能调用
thisOne()
orThisOne()
。我在想(但我可以成功地做到)

目前我有junit测试来测试每个方法,但是这些测试无法检查顺序。我手动检查顺序是否正确,但我希望将其作为测试来编写,以便在扩展DSL时能够重新运行它们

我应该如何测试这一点(顺序)?单元测试是正确的方法吗?有一些图书馆可以这样做吗

编辑

测试的目的是验证DSL。如果DSL是正确的,我确信在
doAfterFirst()
之后,我将拥有
thisOne()
orThisOne()
(因为编译器将对此进行检查)


这个例子很简单,但大多数情况下,DSL使用强制、可选和可重复的方法更大。在这种情况下,当您向DSL添加功能时,您可能会违反一些其他规则。我希望我的测试能够检查任何东西都没有损坏。

我猜您是想从业务角度对API进行健全测试。您可能会编写几个(基于反射的)助手方法来提供帮助

private void assertSequence(Class<? extends Job> jobType, String[] methods) {
   Class currentType = jobType;
   for (String methodName : methods) {
      try {
         Method method = currentType.getMethod(methodName);
         currentType = method.getReturnType();
      } catch (Exception e) {
         Assert.fail(e.toString());
      }
   }
}

/**
 * @param expectedOptions The expected options after invoking the sequence of methods
 */
private void assertSequenceOptions(Class<? extends Job> jobType, String[] methods, String[] expectedOptions) {
   Class currentType = jobType;
   for (String methodName : methods) {
      try {
         Method method = currentType.getMethod(methodName);
         currentType = method.getReturnType();
      } catch (Exception e) {
         Assert.fail(e.toString());
      }
   }
   Set<String> actualMethods = new HashSet<String>();
   for (Method method : currentType.getMethods()) {
      if (!Object.class.equals(method.getDeclaringClass()
             && Modifier.isPublic(method.getModifiers())) 
      {
         actualMethods.add(method.getName());
      }
   }
   assertEquals(actualMethods, new HashSet<String>(Arrays.asList(expectedOptions)));
}

不要这样做。静态语言的美妙之处在于编译器为您做了大量的测试。在
doAfterFirst()
之后,您只能调用
thisOne()
orThisOne()
,因为它是由接口定义的。编译器将在每次编译时检查它。您的代码根本不可能编译。你还想要什么别的订单?您是否担心有人会调用
thisOne()
,然后再调用
doFirst()
?编译器不允许这样做


而是测试整个流畅DSL执行的结果。检查生成器中的代码是否正确。但不要在每一步测试可能的方法

如果您想以流畅的风格为您的fluent API编写基于反射的测试(cf.),您可以使用标记接口标记您的fluent API类,并使用:

标记接口: 测试代码: 自定义断言类: 请注意,我使用Java8StreamsAPI来比较实际方法和预期方法的列表,但是如果您对函数样式不满意,也可以按程序进行比较(再次参见)

import org.assertj.core.api.AbstractAssert;

public class MyFluentAPIAssert extends AbstractAssert<MyFluentAPIAssert, MyFluentAPI> {

    protected MyFluentAPIAssert(MyFluentAPI actual) {
        super(actual, MyFluentAPIAssert.class);
    }

    public static MyFluentAPIAssert assertThat(MyFluentAPI actual) {
        return new MyFluentAPIAssert(actual);
    }

    private Stream<String> getActualChoices() {
        return Arrays.asList(actual.getClass().getMethods()).stream()
                .filter((m) -> !Object.class.equals(m.getDeclaringClass()))
                .map(Method::getName);
    }

    public void hasChoices(String... choices) {
        Stream<String> actualChoices = getActualChoices();
        Stream<String> expectedChoices = Arrays.asList(choices).stream();

        Set<String> actualSet = actualChoices.collect(Collectors.toSet());
        String missing = expectedChoices
                .filter((choice) -> !actualSet.contains(choice))
                .collect(Collectors.joining(", "));

        if (!missing.isEmpty()) {
            failWithMessage("Expected <%s> to have choices <%s>, but the following choices were missing: <%s>", actual.getClass().getName(), String.join(", ", choices), missing);
        }
    }

    public void hasOnlyChoices(String... choices) {
        hasChoices(choices); // first fail if any are missing

        Stream<String> actualChoices = getActualChoices();
        Stream<String> expectedChoices = Arrays.asList(choices).stream();

        Set<String> expectedSet = expectedChoices.collect(Collectors.toSet());
        String extra = actualChoices
                .filter((choice) -> !expectedSet.contains(choice))
                .collect(Collectors.joining(", "));

        if (!extra.isEmpty()) {
            failWithMessage("Expected <%s> to only have choices <%s>, but found the following additional choices: <%s>", actual.getClass().getName(), String.join(", ", choices), extra);
        }
    }
}
import org.assertj.core.api.AbstractAssert;
公共类MyFluentAPIAssert扩展了AbstractAssert{
受保护的MyFluentAPIAssert(MyFluentAPI实际值){
超级(实际,MyFluentAPIAssert.class);
}
公共静态MyFluentAPI资产(MyFluentAPI实际){
返回新的MyFluentAPIAssert(实际);
}
私有流getActualChoices(){
返回数组.asList(实际的.getClass().getMethods()).stream()
.filter((m)->!Object.class.equals(m.getDeclaringClass())
.map(方法::getName);
}
公共选项(字符串…选项){
Stream actualChoices=getActualChoices();
Stream expectedChoices=Arrays.asList(choices).Stream();
Set actualSet=actualChoices.collect(Collectors.toSet());
字符串缺失=预期的选项
.filter((选项)->!actualSet.contains(选项))
.collect(收集器。连接(“,”);
如果(!missing.isEmpty()){
failWithMessage(“应具有选项,但缺少以下选项:”,actual.getClass().getName(),String.join(“,”,选项),缺少);
}
}
public void只有选项(字符串…选项){
hasChoices(choices);//如果缺少任何选项,则首先失败
Stream actualChoices=getActualChoices();
Stream expectedChoices=Arrays.asList(choices).Stream();
Set expectedSet=expectedChoices.collect(Collectors.toSet());
字符串额外=实际选择
.filter((选项)->!expectedSet.contains(选项))
.collect(收集器。连接(“,”);
如果(!extra.isEmpty()){
failWithMessage(“预期只有选项,但找到以下附加选项:”,实际.getClass().getName(),String.join(“,”,选项),额外);
}
}
}

我倾向于同意这一点。您不会编写单元测试来捕获语法错误,那么为什么要编写单元测试来捕获任何其他编译时错误呢。单元测试是为了捕捉运行时错误。我不清楚。我正在考虑这种测试来验证我的DSL。如果DSL是正确的,那么在运行时一切都会正常。这个例子非常简单,但我也可以有可选的、强制的和可重复的方法。有时,当你添加另一种方法时,你会违反一些规则。你无法自动验证DSL是否对你的业务有益。你只需要成为一个好的设计师。你所能测试的就是,如果代码正在做你想让它做的事情(结果和交互),你就不能做一些实际使用DSL的测试,如果你做了突破性的更改,测试将无法编译?@SamHolder我目前正在做。这就是我在问题中手动调用的。例如,如果我期望正好有两个方法和一个“意外”添加第三个方法的更改,那么所有测试都会成功。
String[] sequence = { "doFirst", "doAfterFirst" };
String[] expectedOptions = { "thisOne","orThisOne" };
assertSequence(Job.class, sequence);
assertSequenceOptions(SpecialJob.class, sequence, expectedOptions);
package myfluentapi;

interface MyFluentAPI {
    // marker interface for testing
}

class Job implements MyFluentAPI {
    DoAfterFirst doFirst() { /* ... */ }
}

interface DoAfterFirst extends MyFluentAPI {
    FinishWith doAfterFirst();
}

interface FinishWith extends MyFluentAPI {
    void thisOne();
    void orThisOne();
}
import static myfluentapi.MyFluentAPIAssert.*;

public class JobTest {
    @Test
    public void doAfterFirstHasOnlyTwoChoices() {
        assertThat(new Job().doFirst().doAfterFirst())
          .hasOnlyChoices("thisOne", "orThisOne");
    }
}
import org.assertj.core.api.AbstractAssert;

public class MyFluentAPIAssert extends AbstractAssert<MyFluentAPIAssert, MyFluentAPI> {

    protected MyFluentAPIAssert(MyFluentAPI actual) {
        super(actual, MyFluentAPIAssert.class);
    }

    public static MyFluentAPIAssert assertThat(MyFluentAPI actual) {
        return new MyFluentAPIAssert(actual);
    }

    private Stream<String> getActualChoices() {
        return Arrays.asList(actual.getClass().getMethods()).stream()
                .filter((m) -> !Object.class.equals(m.getDeclaringClass()))
                .map(Method::getName);
    }

    public void hasChoices(String... choices) {
        Stream<String> actualChoices = getActualChoices();
        Stream<String> expectedChoices = Arrays.asList(choices).stream();

        Set<String> actualSet = actualChoices.collect(Collectors.toSet());
        String missing = expectedChoices
                .filter((choice) -> !actualSet.contains(choice))
                .collect(Collectors.joining(", "));

        if (!missing.isEmpty()) {
            failWithMessage("Expected <%s> to have choices <%s>, but the following choices were missing: <%s>", actual.getClass().getName(), String.join(", ", choices), missing);
        }
    }

    public void hasOnlyChoices(String... choices) {
        hasChoices(choices); // first fail if any are missing

        Stream<String> actualChoices = getActualChoices();
        Stream<String> expectedChoices = Arrays.asList(choices).stream();

        Set<String> expectedSet = expectedChoices.collect(Collectors.toSet());
        String extra = actualChoices
                .filter((choice) -> !expectedSet.contains(choice))
                .collect(Collectors.joining(", "));

        if (!extra.isEmpty()) {
            failWithMessage("Expected <%s> to only have choices <%s>, but found the following additional choices: <%s>", actual.getClass().getName(), String.join(", ", choices), extra);
        }
    }
}