如何测试(java)fluent API?
我正在构建一个像这样的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
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);
}
}
}