Screeps 使用 server-mockup 进行测试

screeps 系列教程

前言

在 上篇文章 中,我们介绍了如何使用 jest 测试自己的 screeps 代码,并实现了一个简单 screeps mock 环境。但是假的总归是假的,在测试复杂行为时(如多个房间的资源共享)这个 mock 环境多少会显得有些力不从心,而本文就来介绍一个测试利器:screeps-server-mockup。

简单来说,screeps-server-mockup(下文简称为 mockup)就是一个真正的 screeps 服务器。他使用了 screeps 私服的核心库自己封装了一套接口,让我们可以更加轻易的进行测试。接下来我们要介绍的内容,就是如何使用 mockup 来运行自己的代码,并对其进行测试。按照惯例,我们先来看一下它的优缺点:

引入 server-mockup 的优缺点

优点

  • 真实的环境模拟:不需要再手动进行什么模拟,我们完全可以将其当作是真正的 screeps 环境,只需要把自己的代码丢进去然后启动 mockup 即可。
  • 功能测试:之前的单元测试只能保证最小单元的代码执行可靠性,但是代码可靠不代表功能可靠。在引入 mockup 之后,我们就可以使用它来进行功能测试,例如 pb 采集、新房间占领、lab 集群的运行等等,这些场景通常会持续数十乃至上百 tick。
  • 快速收集数据:由于 mockup 的执行成本足够低(高速运行、不需要人看着、可以自动记录日志 ),所以我们多次运行 bot 并记录运行时数据,通过对比不同启动参数时的 bot 发展情况来进行调优。并且还可以通过这些运行指标量化 bot 的能力,例如多少 tick 能造好 tower、多少时间能升到 RCL 4 等等。

缺点

  • 花费更多精力:和单元测试一样,你同样需要花费时间在完善测试用例上,并且由于测试场景变的更加复杂(创建房间、放置建筑、放置地形、填充资源......),完成一个测试用例需要的时间会变得更长。
  • 更长的测试时间:由于集成测试动不动就要跑几百 tick(几千甚至上万 tick 也有可能),所以每次集成测试的时间可能会持续十几分钟乃至数个小时。
  • 不够直观:由于 mockup 是完全无界面的,我们只能通过运行日志查看 bot 行为,所以那些异常但是不影响正常运行的代码逻辑很难被发现。因此,mockup 不能完全代替手动运行测试。
  • async / await:js 中的异步一直是比较令人头痛的事情,在本文中,我们将第一次在 screeps 中接触到异步操作。本文不会对其进行过多介绍,如果你不太了解的话,推荐在阅读本文时参照 MDN - async 函数。

本文使用 Screeps 使用 Jest 添加单元测试 中所述项目进行升级,请确保你至少读过这篇文章。

依赖安装

话不多说,首先还是安装依赖,执行如下命令来进行安装:

npm install --save-dev screeps-server-mockup fs-extra

如果你安装时出现了问题,那么可能是没有安装 c++ 编译环境导致的,可以参阅 node-gyp build-on-windows 所述来安装 VS 工具包。如下图,在 下载 vs buildTools 之后选中 c++ 生成工具并安装即可。

环境配置

安装完之后我们打开 package.json 来配置一会要用的命令:

  "scripts": {
    "test-unit": "jest src/",
    "test-integration": "jest --runInBand test/integration/",
    "test-cover": "jest --coverage src/"
  }

其中 test-unit 和 test-cover 就是 Screeps 使用 Jest 添加单元测试 中的 test 和 test-c 命令,这里为了统一进行了更名。而 test-integration(集成测试)就是我们今天要介绍的内容。

需要注意的是,我们通过给 jest 传入一个路径参数来限制了测试用例的查找范围,这样可以让单测和集测分开执行。

而且,由于 jest 会默认 并行 执行所有测试套件,而 screeps-server-mockup 只会通过进程方式启动一个数据库实例。所以当并行执行测试时,多个服务器操作同一个数据库会导致出现问题。因此,我们在集成测试中指定了 --runInBand 参数来让 jest 同步执行测试套件。

接下来,我们来简单了解一下如何用 screeps-server-mockup 来执行测试。

测试接入 screeps-server-mockup

首先,我们新建 test/integration/ 目录,我们之后所有的集成测试都会放在这个目录中,然后在其中新建 mockup.test.ts 并填入如下内容:

import { ScreepsServer } from 'screeps-server-mockup'

it('mockup 上手', async () => {
    // 初始化服务器
    const server = new ScreepsServer()
    // 重设服务器
    await server.world.reset()
    // 创建一个基础的 9 房间世界,包含 source 和 controller
    await server.world.stubWorld()

    // 设置 bot 的代码
    const modules = {
        main: `module.exports.loop = function() {
            console.log('Tick!', Game.time)
        }`
    }
    // 设置 bot 的名称和初始位置
    const bot = await server.world.addBot({
        username: 'bot',
        room: 'W0N1', x: 25, y: 25,
        modules
    })

    // 监控 bot 控制台并打印输出
    bot.on('console', logs => logs.forEach(console.log))

    // 启动服务器并运行两个 tick
    await server.start()
    await server.tick()
    await server.tick()

    // 关闭服务器
    await server.stop()
}, 10000)

然后执行 npm run test-integration 就可以看到以下输出:

可以看到测试中成功输出了 bot 控制台的信息(以及一段报错 )。上面的测试用例里已经包含了详细的注释,接下来我们就提一下几个比较重要的点:

  • bot 添加:在测试用例中我们新建了一个 modules 对象,对象的键就是“文件名”,而其值就是“对应文件的代码内容”。

  • 控制台监听:screeps 中的每个 bot 都运行在一个独立的 js 沙盒中,bot 调用的是沙盒中的 console 对象,而不是我们测试环境中的 console。因此,如果想查看 bot 的控制台输出,就要监听 bot 的 console 事件,并将输出“转发”过来。并且也是由于这个原因,代码中的 debugger 会被 js 沙盒吞掉,所以非常的悲催,我们没法在 mockup 中使用断点调试。

  • 运行单个 tick:在 mockup 中,我们可以使用 server.tick() 方法来执行单个 tick,这样就可以对其进行更细粒度的检查,注意 server.tick 之前一定要加 await

  • 修改超时限制:可以看到我们给 it 函数的第三个参数传入了一个值 10000,这个就代表该测试的最大运行时限是 10 秒(默认为 5 秒),由于继承测试经常要运行很久,默认 5 秒的话 jest 就会认为是任务没有完成。你还可以通过 jest.setTimeout(60 * 1000) 来设置全局超时限制。

  • 服务器关闭问题:你应该已经发现了,测试结束时出现了一个报错,这个错误提示是由于 @screeps/common 与游戏数据库连接的 socket 处理不充分导致的,并不会对测试造成影响,后面我们会将其隐藏掉。

我在 ts 中调用 screeps-server-mockup 为什么没有类型补全?

如果你自己开始写测试用例的话就会发现这个问题,实际上 mockup 是有类型声明文件的,但是由于其发布问题导致 tsc 无法发现其类型定义,你可以通过在 node_modules\screeps-server-mockup\package.json 的末尾添加 "types": "dist/src/main.d.ts" 字段然后重启代码编辑器来解决这个问题。

server-mockup 基本使用

现在我们来简单介绍一下 mockup 的用法,这一小节一共包含三部分,分别是:服务器操作、设置游戏世界操作以及读取游戏世界。记住,以下 api 都是异步函数,所以不要忘了带上 await

1、服务器操作

我们通过 new ScreepsServer() 得到的 server 就是服务器实例,我们要用到的服务器操作都暴露在这个对象上,一共只有三个:

  • server.start:启动服务器,需要在 tick 方法之前调用
  • server.tick:运行单个 tick
  • server.stop:关闭服务器,若不调用将导致测试进程无法退出

这个很简单对吧,看名字就很清楚了,要注意的是 start 方法每个 server 实例只能调用一次,多次调用会导致无法正确释放服务器连接。

2、设置游戏世界

在 server 实例上还有一个名为 world 的属性,我们就可以通过这个对象对游戏世界进行设置,这个对象暴露了以下常用操作:

  • server.world.addRoom:新增一个房间,房间名为接受的参数,注意该方法只会创建一个完全空白的房间,没有地形数据、没有任何 controller 之类的游戏对象。
  • server.world.setTerrain:给一个房间设置地形数据,我们可以使用 mockup 导出的 TerrainMatrix 来设置一个地形,用法可以在 这里 里找到。
  • server.world.addRoomObject:在指定房间中新增一个游戏对象,诸如 controller、source、creep、spawn 等等任何物体,我们搭建测试环境主要靠的就是这个方法,用法可以在 这里 里找到。
  • server.world.addBot:添加一个 bot,说是 bot,其实就是一段可以执行的代码。我们可以用它设置 bot 的名字、出生点位置、代码内容等,并且返回的 bot 实例也可以访问其状态。

3、读取游戏世界

在 server.world 和 bot 实例上同样也暴露了很多接口用于对游戏世界进行查询,下面列举一些比较常见的:

  • server.world.gameTime:获取当前的游戏 tick
  • server.world.getTerrain:获取指定房间的地形数据
  • server.world.roomObjects:获取指定房间的游戏对象
  • bot.memory / .username / .cpu / .rooms:获取对应 bot 的内存、用户名、cpu 或者可见的 room。

如果你足够了解 screeps 的话,那么应该记得 screeps 默认使用 lokijs 作为数据库。在 mockup 中,我们可以通过如下形式获取到游戏的实际数据库,然后就可以使用这个数据库实例对游戏内容进行更为全面的读写:

// db 就是实际的 lokijs 数据库
const { db } = await server.world.load();

至于如何使用这里就不在赘述,可以参考 lokijs - 官方示例 或者 screeps-server-mockup - addBot 源码(这个文件里还有很多使用 loki 的地方 )。


对 server-mockup 的介绍就到这里,上面只列举了一些主要操作,你可以从 screeps-server-mockup 官方示例 了解到更多用法,或者打开你的项目,node_modules\screeps-server-mockup\dist\src\main.d.ts 这个文件就是 server-mockup 的声明文件,并没有多少 api,ctrl 一路点过去大致读一遍即可掌握其用法。

通过这些方法,我们可以构建出任何我们想要进行测试的场景,例如两个 creep 战斗、多个房间中的 terminal 共享、factory 的生产线合作等等。但是...还有一个问题,我们怎么获取到自己的代码呢,总不能编译完手动复制过来吧,没有关系,下一小节我们将借助 node 的能力来实现这个功能。

构建并在 mockup 中执行自己的 bot

首先我们来改造一下咱们的 npm 命令,如下:

"test-integration": "npm run build && jest test/integration/",

我们在执行集测之前先执行 npm run build 来进行代码构建,由此来保证执行集测时 dist/ 目录下肯定存在已经编译好的代码,接下来在 test/ 中新建 moduleUtils.ts 并填入如下内容:

import { readFile } from 'fs'

/* addBot 需要的 bot module */
interface BotModule {
    /* 主代码文件 */
    'main': string
    /* sourceMap 文件 */
    "main.js.map": string
}

/**
 * async 版本的 readFile
 * 
 * @param path 要读取的文件
 * @returns 该文件的字符串内容
 */
const readCode = async function (path: string): Promise {
    return new Promise((resolve, reject) => {
        readFile(path, (err, data) => {
            if (err) return reject(err);
            resolve(data.toString())
        })
    })
}

/**
 * 全局唯一的 dist 代码模块
 */
let myCode: BotModule

/**
 * 获取自己的全量代码模块
 */
export const getMyCode = async function (): Promise {
    if (myCode) return myCode

    const [ main, map ] = await Promise.all([
        readCode('dist/main.js'),
        readCode('dist/main.js.map.js')
    ])

    myCode = { 'main': main, 'main.js.map': map }
    return myCode
}

这里我们使用 fs.readFile 去读取并缓存 dist 目录下的文件,然后将其转换成字符串。这样,我们只需要调用 getMyCode 就可以直接获取 server.world.addBot 中需要的 modules,如下:

import { getMyCode } from '../moduleUtils';

it('执行实际代码', async () => {
    const server = new ScreepsServer()
    // 一系列场景搭建操作

    // 获取我的代码,并添加到 bot 中
    onst modules = await getMyCode()
    await server.world.addBot({ username: '战斗测试 A 单位', room: 'W0N1', x: 25, y: 25, modules });

    // 测试内容...
})

复用 server 实例

如果你之前有服务器开发经验的话,你就会敏锐的察觉到在每个测试用例里都新建一个 mockup server 实例并不优雅,是的,我们完全可以通过 单例模式让不同的测试复用同一个 server 实例。本小节将介绍如何进行复用并减少测试用例中的代码量:

首先在 test/ 目录中新建 serverUtils.ts 并填入如下内容:

import { ScreepsServer } from 'screeps-server-mockup'

/**
 * 全局唯一的 server 实例
 */
let server: ScreepsServer

/**
 * 获取可用的 server 实例
 * @returns server 实例
 */
export const getServer = async function () {
    if (!server) {
        server = new ScreepsServer()
        await server.start()
    }

    return server
}

/**
 * 重置 server
 */
export const resetServer = async function () {
    if (!server) return
    await server.world.reset()
}

/**
 * 停止 server
 */
export const stopServer = async function () {
    if (!server) return

    // monkey-patch 用于屏蔽 screeps storage 的退出提示
    const error = global.console.error
    global.console.error = (...arg) => !arg[0].match(/Storage connection lost/i) && error.apply(this, arg)

    await server.stop()
}

可以看到我们封装了三个方法,分别用于获取、重置和关闭服务器。

然后我们打开 test/setup.ts,来把服务器的重置和关闭配置到 jest 测试流程中:

import { refreshGlobalMock } from './mock'
import { resetServer, stopServer } from './serverUtils'

// 之前的单测环境 mock
refreshGlobalMock()
beforeEach(refreshGlobalMock)
// 每次测试用例执行完后重置服务器
afterEach(resetServer)
// 所有测试完成后关闭服务器
afterAll(stopServer)

这样配置好后,我们就不需要再关心服务器的释放问题了,下面就是一个简单的使用示例:

import { getServer } from '../serverUtils'

it('封装后的 server 运行流程', async () => {
    const server = await getServer()
    await server.world.stubWorld()
    await server.tick()
})

我们可以把这个测试用例复制两遍后执行一下 npm run integration,可以看到第二次复用了 server,所以测试速度大幅提升,并且结尾的报错也清理掉了

在完成了上面的流程之后,另外一个严峻的问题摆在我们面前,怎么查看 server 中 bot 的运行情况?把所有的东西都打印在控制台上?不太好吧,如果出现问题了再打印,感觉能获取到的信息比较少,但是如果提前把所有信息都打印出来,那控制台就会变得乱糟糟的,还有可能因为缓冲区的上限导致新 log 很快就把之前的 log 顶掉了。在下个小节,我们就来解决这个问题。

使用 json 记录 bot 运行情况

由于我们是要记录代码的运行,所以可以在每个 tick 开始时监听 bot 的控制台和内存,并在测试结束后将其写入到 json 里。这样不仅可以让控制台保持清爽,还可以非常全面的记录运行情况。

我们在 test/ 目录下新建 logRecorder.ts 并填入如下内容:

import { resolve } from 'path'
import { outputJson } from 'fs-extra'
import { ScreepsServer } from 'screeps-server-mockup'
import User from 'screeps-server-mockup/dist/src/user'

/** tick 日志 */
interface TickLog {
    /** 该 tick 的控制台输出 */
    console?: string[]
    /** 该 tick 的通知信息 */
    notifications?: string[]
    /** 该 tick 的 Memory 完整拷贝 */
    memory?: AnyObject
}

const now = new Date()
/**
 * 本次测试日志要保持到的 log 文件夹路径
 * 形如:项目根目录/server/logs/2021-3-15 17-58-45
 */
const LOG_DIR = resolve(__dirname, `../server/logs/${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`)

/**
 * 日志记录者
 * 用于记录指定 bot 的运行日志
 */
export default class LogRecorder {
    /**
     * 保存到的日志文件名称
     */
    readonly name: string
    /**
     * 日志内容
     */
    logs: { [tick: number]: TickLog } = {}

    /**
     * 实例化日志记录
     * 
     * @param name 日志要保存到的文件名称
     * @param server 日志记录所在的游戏服务器
     * @param bot 要进行记录的 bot 实例
     */
    constructor(name: string, server: ScreepsServer, bot: User) {
        this.name = name

        // 记录控制台输出
        const consoleListener = async logs => {
            this.record(await server.world.gameTime, { console: logs });
        }

        // 记录 Memory 和通知信息
        const tickListener = async () => this.record(await server.world.gameTime, {
            memory: JSON.parse(await bot.memory),
            notifications: (await bot.newNotifications).map(({ message }) => message)
        })

        // 服务器重置时代表记录完成,取消回调并保存日志到本地
        const resetListener = () => {
            this.save()
            server.removeListener('tickStart', tickListener)
            bot.removeListener('console', consoleListener)
            server.removeListener('reset', resetListener)
        }

        // 注册回调
        server.on('tickStart', tickListener)
        bot.on('console', consoleListener);
        server.on('reset', resetListener)
    }

    /**
     * 记录新的日志
     * 
     * @param tick 要保存到的 tick
     * @param newLog 日志内容
     */
    record(tick: number, newLog: TickLog) {
        if (!this.logs[tick]) this.logs[tick] = newLog
        else {
            Object.keys(newLog).forEach(key => {
                if (key in this.logs[tick]) return
                this.logs[tick][key] = newLog[key]
            })
        }
    }

    /**
     * 保存日志到本地
     */
    async save() {
        await outputJson(resolve(LOG_DIR, `${this.name}.json`), this.logs, {
            spaces: 4
        })
        // 主动移除内存中的日志,加速垃圾回收
        this.logs = []
    }
}

稍微有点长,不过不用担心,我们简单介绍一下,这个文件里的 LogRecorder 可以监听一个 bot,并在 tick 开始和 bot 控制台输出内容时进行记录,最后在服务器重置时将日志写入 项目根目录/server/logs/测试执行日期/bot名称.json 中。

但是现在它还没法正常使用,因为 server-mockup 并没有提供诸如 tick 开始,服务器重置这些事件,所以接下来我们就着手升级一下 mockup:

打开 serverUtils.ts,首先在开头引入我们的日志记录器:

import LogRecorder from './logRecorder'

然后添加升级函数:

/**
 * 升级 server 实例
 * 添加更多的事件发射,并添加自动日志记录
 * 
 * @param server 要进行升级的 server 实例
 * @returns 升级后的 server 实例
 */
const upgradeServer = function (server: ScreepsServer) {
    const { tick } = server
    // 发射 tick 开始事件
    server.tick = async function () {
        server.emit('tickStart')
        return tick.apply(this)
    }

    const { reset, addBot } = server.world
    // 发射服务器重置事件
    server.world.reset = async function () {
        server.emit('reset')
        return reset.apply(this)
    }
    // 在添加 bot 时同步启动日志记录实例
    // 会在 server reset 时自动保存并释放
    server.world.addBot = async function (...args) {
        const [ addBotOptions ] = args
        const { username } = addBotOptions

        const bot = await addBot.apply(this, args)
        new LogRecorder(username, server, bot)

        return bot
    }

    return server
}

最后在新建 server 时调用升级函数:

export const getServer = async function () {
    if (!server) {
        server = upgradeServer(new ScreepsServer()) // <== 看这里
        await server.start()
    }
    // ...
}

你或许发现了,我们在 upgradeServer 中也改造了 server.world.addBot 方法,并在其中实例化了日志记录。这样,我们在写测试用例时不需要做任何事情,日志记录就会在 bot 添加好后开始,并在测试用例结束(服务器重置)时自动保存。

接下来我们测试一下,执行如下测试用例:

it('日志记录测试', async () => {
    const server = await getServer()
    await server.world.stubWorld()

    const modules = {
        main: `module.exports.loop = function() {
            console.log('Tick!',Game.time);
            Memory[Game.time] = Game.time
        };`
    };

    await server.world.addBot({ username: 'bot 启动', room: 'W0N1', x: 25, y: 25, modules });

    for (let i = 0; i < 10; i++) {
        await server.tick()
    }
})

npm run test-integration 执行成功之后你就可以在 server/logs/测试执行日期/ 中找到这个 bot 的执行日志,其中每个键值对都是一个 tick,memory 字段为当时 Memory 的完整拷贝,而 notifications 则是使用 Game.notify 发送的通知:

由于日志记录是和 bot 创建绑定的,所以哪怕你在一个测试用例里添加多个 bot 也不会影响日志的正常记录。不过这里我们只保存 bot 内部的信息(内存、控制台、通知)而没有保存周围环境(房间参数、source、controller 状态等),你可以通过在 bot 内部添加统计模块来将环境状态记录到内存,或者直接读取服务器数据库来将状态写入到日志。

我把服务器日志打印到了控制台,但是格式太乱了,怎么解决?

由于 jest 会自动的对 console 模块进行改造,由此来提供更多的调试信息,所以当我们在控制台打印信息时会显得更加的“啰嗦”:

包含 jest 调试信息的 console

我们可以通过在 test/setup.ts 里添加如下代码的形式来还原 console:

import { log } from 'console'
global.console.log = log
还原了 console 之后的控制台输出

但是请记住,在测试时应尽量少的在控制台打印无关内容,防止出现问题时影响原因的查找。

看到这里,我们已经介绍了 server-mockup 的绝大多数内容,但是还是存在一个问题,如果我只想测试单个模块呢?例如一个比较后期的多房间资源共享。装进服务器的代码可是一个完整的 bot,我总不能等他慢慢发展到后期吧(又或者我的 bot 根本不具备冷启动能力 )。而且由于所有模块都包含在里边,万一问题是其他模块导致的,这不就让问题溯源更加复杂了么。

那么,能不能只把要测试的模块装进服务器然后进行测试呢?答案是可以的,这也就是我们接下来要讲的内容。

在 server-mockup 中进行功能测试

想要进行功能测试(又称行为测试 ),最重要的是进行局部编译,由于 mockup 只能接受纯 js 代码,所以我们需要把要测试的模块单独拿出来进行编译,一提到编译,怎么少得了我们的老朋友 rollup 呢?复习一下,rollup 可以接受一个入口文件,然后将其依赖按照指定的配置编译成纯 js 代码,这简直完美契合我们的需求。

rollup 除了命令行调用之外(我们之前编译 src 目录下的代码就是使用的命令行调用 )还支持 api 调用,这种调用方式更加的灵活,我们接下来就用这种方式来实现 编译指定文件并获取其 js 代码字符串

要进行的修改也很简单,我们打开 test/moduleUtils.ts 并在其中添加如下代码:

import resolve from '@rollup/plugin-node-resolve'
import { InputOptions, OutputOptions, RollupBuild } from 'rollup'
const { rollup } = require('rollup')
const commonjs = require('@rollup/plugin-commonjs')
const typescript = require('rollup-plugin-typescript2')

/**
 * 构建流程
 */
const plugins = [
    // 打包依赖
    resolve(),
    // 模块化依赖
    commonjs(),
    // 编译 ts
    typescript({ tsconfig: 'tsconfig.json' })
]

/**
 * 执行模块构建
 * 
 * @param inputPath 构建的入口文件
 * @returns 可以直接用在 bot 中的 module
 */
export const build = async function (input: string): Promise {
    const inputOptions: InputOptions = { input, plugins }
    const outputOptions: OutputOptions = { format: 'cjs', sourcemap: true }

    // 构建模块代码
    const bundle: RollupBuild = await rollup(inputOptions);
    const { output: [targetChunk] } = await bundle.generate(outputOptions);

    // 组装并返回构建成果
    return {
        'main': targetChunk.code,
        'main.js.map': `module.exports = ${targetChunk.map};`
    }
}

我们要执行编译的话,只需要调用 build 方法并传入要构建入口文件的路径即可。仔细观察一下就能发现,这个构建流程几乎和我们在 rollup.config.js 中配置的一模一样。

接下来新建 test/behavior 目录,我们所有的功能测试都将写在这个目录下。测试文件的结构是这样的,每个功能测试都是一个文件夹,其中包含了编译入口文件 main.ts 和测试文件 main.test.ts

其中,编译入口文件的作用和我们的 src/main.ts 一样,都导出了一个 loop 函数,我们会把要进行测试的模块单独引入进来并调用。然后我们将在 main.test.ts 中用刚才的 build 方法从这个入口文件进行构建,最终得到可以在 mockup 中可以执行的代码对象。

接下来我们就按照刚才的步骤进行测试,假设我们在 src/modules/getRoom.ts 中有如下方法:

export const getRoom = function () {
    const roomNames = Object.keys(Game.rooms)
    console.log(roomNames.join(','))
}

这个“模块”的作用很简单,就是获取所有可见的房间。接下来我们就要单独对它进行测试,首先新建 test/behavior/getRoom/main.ts 并添加如下代码:

// 路径别名 @ 的用法详见 https://www.jianshu.com/p/f81e2d6092a1
import { getRoom } from '@/modules/getRoom'

export const loop = () => {
    getRoom()
}

然后在新建 test/behavior/getRoom/main.test.ts 并添加如下代码,这段代码会新建房间并把 bot 防止在 W1N1 房间,然后监听控制台是否会输出正确的内容:

import { resolve } from 'path'
import { getServer } from '@test/serverUtils'
import { build } from '@test/moduleUtils'

it('getRoom 可以输出房间名到控制台', async () => {
    const server = await getServer()
    await server.world.stubWorld()

    const spawnRoomName = 'W1N1'

    // 从入口文件构建并添加进 bot
    const modules = await build(resolve(__dirname, './main.ts'))
    const bot = await server.world.addBot({ username: 'getRoom 测试', room: spawnRoomName, x: 25, y: 25, modules })

    // 断言 console 输出并跑 tick
    bot.on('console', logs => expect(logs).toEqual([spawnRoomName]))
    await server.tick()
}, 10000)

现在我们就可以来执行功能测试了,打开 package.json,在 scripts 中新增如下命令:

"scripts": {
  "test-behavior": "jest --runInBand test/behavior/",
},

然后执行 npm run test-behavior 即可看到测试通过:

由此,我们就通过指定入口文件的方式来对要测试的功能进行单独编译,下面留几个思考题,大家可以尝试一下:

  • 执行集成测试时是通过执行 npm run build 来获取代码的,能否通过上面的 build 函数进行生成?
  • build 函数中的构建流程和 rollup.config.js 中的流程类似,这两者能否进行复用?

总结

本文介绍了如何使用 screeps-server-mockup 来进行真实测试,并对 mockup 的调用和日志记录进行了一定程度的封装。最后通过调用 rollup 进行局部构建来实现功能测试。由此,我们就可以对 screeps 的游戏代码进行更加真实的测试来确保其功能真实可用。

至此,搭建 screeps 开发环境系列教程已全部结束了,想要查看更多教程?欢迎访问 《Screeps 中文教程》!

你可能感兴趣的:(Screeps 使用 server-mockup 进行测试)