【问题标题】:How to test methods which depend on initial parameters?如何测试依赖于初始参数的方法?
【发布时间】:2019-07-23 20:21:38
【问题描述】:

在我的测试类中,我有几个参数化方法,其中参数是对象初始化的一部分。我对它们都使用相同的参数,然后我测试依赖于一个或多个参数的方法。

@MethodSource(value = "validSizes")
@ParameterizedTest
void testGetHeight(int terrainWidth, int height) {
    World world = new World(terrainWidth, height);
    assertEquals(height, world.getHeight());
}

这可能会导致代码重复——我需要重复参数、World 构造和@MethodSource 注释。

我想避免代码重复,因为如果我向构造函数添加另一个参数,我将不得不更改所有依赖于参数的测试方法。

我可以使用@BeforeEach 注释来做到这一点,即参数化setUp() 方法,还是用其他方式?用不同的参数测试这些方法是一种好习惯吗?

【问题讨论】:

  • 在这种情况下,您无论如何都需要更改所有测试方法,那么有什么区别?您可以将 World 创建移动到一些 getWorld() 方法,但您仍然需要提供 height 值来断言等。但是这又有什么问题呢?您正在更改构造函数,所以很明显测试会改变
  • 如果我在setUp() 方法中这样做,我只需要改变它。但是,我找不到参数化 setUp() 方法的方法。
  • 这很明显 - 参数化是在“当前测试运行”上下文中进行的 - 你将如何参数化普通的普通方法?

标签: java junit parameters junit5


【解决方案1】:

您可以编写自己的参数提供程序,它提供一个完全构造的世界对象并将其注入到您的 setup/BeforeEach 方法中(您将其存储在您的测试方法中使用)。这样您就可以最小化您的设置代码并将您的初始化逻辑浓缩到代码中的一个特定位置。

编辑:我想到了一个错误的机制。所以这里是正确的例子,它澄清了我的意思:

让我们假设你的世界对象看起来像这样:

class World {
    private final int width;
    private final int height;

    public World(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public String toString() {
        return "World{" + "width=" + width + ", height=" + height + '}';
    }
}

您有几个测试方法,您需要一组不同的 World 对象(可能是动态创建的),如下所示:

class ArgumentProviderTest {
    @ParameterizedTest
    @ArgumentsSource(WorldArgumentProvider.class)
    void yourFirstTest(World w) {
        System.out.println(w);
    }

    @ParameterizedTest
    @ArgumentsSource(WorldArgumentProvider.class)
    void yourSecondTest(World w) {
        System.out.println(w);
    }
}

你看,测试都期望一个 World 对象作为参数。现在我们需要一种方法来提供世界对象。为此,我们编写了自己的 ArgumentsProvider,它也很简单:

class WorldArgumentProvider implements ArgumentsProvider {

    private Collection<World> createWorlds() {
        // Somehow create a bunch of World objects
        // !!! Here goes your initialization logic. !!!
        return ThreadLocalRandom.current()
                .ints()
                .limit(10)
                .mapToObj(i -> new World(i % 100, i / 100))
                .collect(Collectors.toList());
    }

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        // create an Arguments object from these (discrete) worlds.
        // remember: one method takes one Arguments object which carries each single argument/parameter
        return createWorlds().stream().map(Arguments::of);
    }
}

在这里,您可以使用完整的编译器支持做任何您想做的事情(而不是使用方法名称作为字符串)。

也许您可以使用带有 @TestFactory 的 DynamicTests 实现类似的行为,但这取决于您的确切测试。


我的第一个想法是使用 ParameterResolver,它的工作原理基本上是这样的:

  1. 将 ParameterResolver 注册为扩展(在此上下文中,在类级别而不是方法级别)
  2. 为您的测试和生命周期方法 (BeforeEach/AfterAll/...) 的每个(未解决的)参数调用此解析器
  3. 它决定它是否真的可以解析这个参数(例如,正确的类型、正确的索引等)
  4. 它提供实际的 Argument 作为参数

但是,通过这种机制,你不能告诉 Jupiter 引擎为一个方法构造多个 Test 实例(Jupiter 为这样的事情引入了 TestTemplate 的概念)。有趣的是,这个 Parameterized-Class conect 是在 jUnit-4 中实现的(作为参数化测试的唯一方法),并且是 AFAIK 有望在 Jupiter 未来出现的功能。我很期待。

【讨论】:

  • Argument Provider 的好主意,它启发了我使用包装类。但是假设我想实现参数提供程序 - 你能详细说明它将如何将对象注入我的设置代码吗?
【解决方案2】:

您可以使用包装类将所有初始化代码放在一个位置。

class WorldWrapper {
    public final World world;
    public final int terrainWidth;
    public final int height;

    //constructor to initialize the fields
}

您仍然需要使用 @MethodSource 注释每个方法并重复单个参数,但如果您决定更改构造函数参数,则不必担心。

@MethodSource(value = "validSizes")
@ParameterizedTest
void testGetHeight(WorldWrapper param) {
    assertEquals(param.height, param.world.getHeight());
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-12-30
    • 1970-01-01
    • 1970-01-01
    • 2015-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多