【问题标题】:How to test a (java) fluent API?如何测试(java)流利的 API?
【发布时间】:2015-11-10 14:28:17
【问题描述】:

我正在构建一个类似这样的 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 而不是Builder,所以我想测试方法的顺序。例如,我想测试如果我调用doAfterFirst(),我现在只能调用thisOne()orThisOne()。我正在考虑类似的事情(但我可以成功实现)

@Test
public void testDoAfterFirstChoice() {
    String expectedResults = new String[] {"thisOne","orThisOne"};
    assertTrue(new Job().doFirst().doAfterFirst().availableChoices().equals(expectedResults));
}

目前我有 junit 测试来测试每个这种方法,但是这个测试无法检查顺序。我手动检查顺序是否正确,但我想将其编写为测试,因此如果我扩展我的 DSL,我将能够重新运行它们。

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

编辑

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

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

【问题讨论】:

    标签: java unit-testing junit junit4 fluent


    【解决方案1】:

    不要这样做。静态语言的美妙之处在于编译器为你做了很多测试。在doAfterFirst() 之后,您只能调用thisOne()orThisOne(),因为它是由接口定义的。编译器会在每次编译时检查它。您的代码根本没有其他可能编译。您还期望什么其他订单?你害怕有人会打电话给thisOne(),然后是doFirst()吗?编译器不允许这样做

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

    【讨论】:

    • 我倾向于同意这一点。您不会编写单元测试来捕获语法错误,那么为什么要编写单元测试来捕获任何其他编译时错误。单元测试是为了捕捉运行时错误。
    • 我不清楚。我正在考虑这种测试来验证我的 DSL。如果 DSL 是正确的,那么在运行时一切都会好起来的。这个例子很简单,但我也可以有可选的、强制的和可重复的方法。有时,当您添加另一种方法时,您会违反一些规则。
    • 您无法自动验证 DSL 是否适合您的业务。你只需要成为一个好的设计师。您可以测试的只是代码是否正在执行您希望它执行的操作(结果和交互)
    • 你不能只做一些实际使用 DSL 的测试,如果你做出重大更改,测试将无法编译?跨度>
    • @SamHolder 我目前正在这样做。这就是我在我的问题中所说的手动。例如,如果我期望恰好有两种方法,并且“意外”添加了第三种方法,那么所有测试都会成功。
    【解决方案2】:

    我猜您想从业务角度对您的 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)));
    }
    

    用法

    String[] sequence = { "doFirst", "doAfterFirst" };
    String[] expectedOptions = { "thisOne","orThisOne" };
    assertSequence(Job.class, sequence);
    assertSequenceOptions(SpecialJob.class, sequence, expectedOptions);
    

    【讨论】:

      【解决方案3】:

      如果您想为您的 Fluent API 以 fluent 风格编写基于反射的测试(参见 Lance Java's answer),您可以使用标记接口标记您的 fluent API 类并使用 AssertJ custom assertions

      标记界面:

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

      自定义断言类:

      请注意,我使用 Java 8 Streams API 来比较实际方法和预期方法的列表,但如果您对功能风格。

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

      【讨论】:

        猜你喜欢
        • 2015-08-29
        • 1970-01-01
        • 1970-01-01
        • 2010-12-10
        • 2020-10-06
        • 1970-01-01
        • 1970-01-01
        • 2017-11-24
        • 1970-01-01
        相关资源
        最近更新 更多