如何在保证随机性的情况下对减速器进行单元测试? (还原)

How to unit-test a reducer with guaranteed randomness? (Redux)

我正在尝试创建一个 react/redux 非标准 "todo" 应用程序。所以我选择了 2048,这是一个相当有趣的游戏,我可以 fiddle 玩。

我有游戏 "working"(游戏按规则进行),现在我想追溯主动添加一些测试用例,以确保我不会破坏任何重要的东西行(并且,你知道,练习)。

我为板设置了一个减速器,用于监听不同的动作:

const Board = (state = 0, action) => {
    if(state === 0){
        return createBoard();
    }
    switch (action.type) {
        case 'MOVE_UP':
            return move(state, 0);
        case 'MOVE_RIGHT':
            return move(state, 1);
        case 'MOVE_DOWN':
            return move(state, 2);
        case 'MOVE_LEFT':
            return move(state, 3);
        case 'NEW_GAME':
            return createBoard();
        default:
            return state;
    }
}

但是 move(board, direction) 函数完成了大部分繁重的工作,这就是问题所在:

function move(board, direction){
    //0: up, 1: right, 2: down, 3: left
    var b;
    switch(direction){
        case 0:
            b = mergeBoardUp(board);
            break;
        case 1:
            b = mergeBoardRight(board);
            break;
        case 2:
            b = mergeBoardDown(board);
            break;
        case 3:
            b = mergeBoardLeft(board);
            break;
        default:
            b = board.map(x => x);
    }
    if(distinctBoard(board, b)){
        b = addNewTile(b);
    }
    return b;
}

我的理解是reducer应该做黑盒测试(不要试图拉出测试"mergeBoardUp"功能,测试使用后的状态)。由于游戏的性质,每当下一个有效的动作时都会添加一个新的板块 (if(distinctBoard){addNewTile})。 我有一组非常好的测试用例,我想在测试板旋转和合并方面实施,但是我想不出解决 "random" 磁贴放置的方法。

示例测试用例供参考:

it("MoveUp - simple", () => {
    var input = [[2,0,0,0],
                [2,4,8,16],
                [2,4,0,16],
                [2,4,8,0]];

    var inputHistory = {
        past: [],
        present: input,
        future: []
    };

    var expected = [[4,8,16,32],
                    [4,4,0,0],
                    [0,0,0,0],
                    [0,0,0,0]];

    var rotatedLeft = Board(inputHistory, MovementButtons.moveUp()).present;

    expect(rotatedLeft).toBe(expected);
})

上面失败了,因为在有效移动之后,一个新的瓷砖被随机放置在数组中,导致 "expected" 数组总是偏离一个值。

在这种情况下进行测试的合适方法是什么? 我的直觉告诉我要将我想测试的函数分离到一个实用程序文件中(并测试从中导出的函数),但这似乎是测试 reducer 的反模式。

这里真正的问题是随机性不应该发生在reducer中。减速器应该是 "pure functions",没有副作用。 不是 纯的 Reducer 代码仍然 运行,但时间旅行调试将无法按预期工作,正如您所发现的,测试变得困难。

有几种方法可以考虑重组。您可以将大部分逻辑放入 action creator 中,并让 reducer 逻辑根据需要简单地将数据合并到位。另一种方法可能是让动作创建者生成所需的任何随机性,并将其包含在动作中。这样,reducer 本身就会保持纯净、确定性和可测试性。

我会再问,我要测试什么?看起来您可以测试两种不同的功能:MoveUp 的合并和 MoveUp 的随机新行。

看来您可以简单地对 MoveUp 的合并功能进行正确的单元测试:

    expect(rotatedLeft.slice(0,3)).toEqual(expected.slice(0,3));

通过正确捕获前三行的合并,我认为这足以作为 MoveUp 合并功能的单元测试。

然后单独地,您可以通过检查最后一行中的每个值是否属于可能的随机值集来测试最后一行是随机的。

    for (var i = 0; i < rotatedLeft[3].length; i += 1) {
        expect([0, 2, 4]).toContain(rotatedLeft[3][i]);
    }