从头搭建rpc框架_#LearnByDIY-如何从头开始创建JavaScript单元测试框架

何涵衍
2023-12-01

从头搭建rpc框架

by Alcides Queiroz

通过Alcides Queiroz

#LearnByDIY-如何从头开始创建JavaScript单元测试框架 (#LearnByDIY - How to create a JavaScript unit testing framework from scratch)

I promise, this is gonna be fun. =)

我保证,这会很有趣。 =)

Probably, automated tests are part of your daily routine (if not, please stop reading this article and start from the beginning, by learning from the father of TDD himself). You’ve been using testing frameworks such as Node-tap (or Tape), Jasmine, Mocha or QUnit for quite a while, just accepting that they do some magic stuff and not asking too many questions about them. Or, if you’re like me, maybe you’re always curious about how things work, including testing frameworks, of course.

自动化测试可能是您日常工作的一部分(如果不是,请从TDD的父亲那里学习,停止阅读本文, 从头开始 )。 您已经使用了诸如Node-tap (或Tape ), JasmineMochaQUnit之类的测试框架已有相当长的时间了,只接受它们做一些神奇的事情,而又不问他们太多问题。 或者,如果您像我一样,也许您总是对事物的工作方式(包括测试框架)总是很好奇。

This article will guide you through the process of creating a JavaScript testing framework from scratch, with a pretty decent DSL and a nicely detailed output. This is the first article in my #LearnByDIY series. The idea is to demystify certain kinds of software that we’re used to, by creating simpler versions of them.

本文将指导您从头开始创建JavaScript测试框架的过程,并提供相当不错的DSL和非常详细的输出。 这是我的#LearnByDIY系列文章中的第一篇。 这个想法是通过创建较简单的版本来使我们习惯的某些软件神秘化。

免责声明 (Disclaimers)

Before starting, some important notes:

开始之前,请注意以下重要事项:

  • The goal of this article is not to create a production-ready tool. Please, don’t use the framework that we’ll be creating to test production code. Its purpose is purely educational. =)

    本文的目的不是创建可用于生产的工具。 请不要使用我们将要创建的框架来测试生产代码。 其目的纯粹是教育性的。 =)

  • Naturally, our little framework won’t be full-featured. Things such as async tests, parallel executions, a richer set of matchers, a CLI (with options like --watch), pluggable reporters and DSLs, etc., won’t be present in our final version. However, I strongly recommend that you keep toying with this project and maybe try to implement some of these missing parts. Perhaps you can transform it into a serious open source project. I’d love to know that this toy project became an “actual” testing framework.

    自然,我们的小框架不会具有完整的功能。 诸如异步测试,并行执行,更丰富的匹配器,CLI(带有--watch选项),可插入报告器和DSL等内容将不会出现在我们的最终版本中。 但是,我强烈建议您继续研究该项目,并尝试实现其中一些缺少的部分 。 也许您可以将其转变为一个严肃的开源项目。 我很想知道这个玩具项目已成为“实际”测试框架。

T️Tyrion-一个微小的测试框架 (⚔️ Tyrion - A tiny testing framework)

Our framework will be tiny, but “brave” for its size. So, there’s no better name than Tyrion (yeap, he’s my favorite GoT character, too).

我们的框架将很小,但是就其规模而言是“勇敢的”。 因此,没有比提利昂更好的名字了(是的,他也是我最喜欢的GoT角色)。

We’ll be using Node.js in this project, with good and old CommonJS modules. The minimum Node version you’ll need is v8.6.0. If you have an older version, please update it.

我们将在该项目中使用Node.js,同时使用旧的CommonJS模块。 您需要的最低Node版本是v8.6.0。 如果您使用的是旧版本,请进行更新。

Oh, I almost forgot… I’m using Yarn throughout this article, for things like yarn init, yarn link and so on, but you can use “vanilla” NPM in a similar manner (npm init, npm link, …).

哦,我差点忘了……我在整篇文章中都使用Yarn进行诸如yarn inityarn link类的事情,但是您可以以类似的方式使用“ vanilla” NPM( npm initnpm link等)。

创建项目文件夹结构 (Creating the project folder structure)

First, let’s create the following folder structure:

首先,让我们创建以下文件夹结构:

tyrion/||______ proj/|      ||      |______ src/||______ playground/       |       |______ src/       |______ tests/

In other “words”:

换一种说法”:

$ mkdir -p tyrion/proj/src tyrion/playground/src tyrion/playground/tests

We need two folders, each one to a separate project.

我们需要两个文件夹,每个文件夹到一个单独的项目。

  • The proj folder will contain the Tyrion framework package.

    proj文件夹将包含Tyrion框架包。

  • The playground folder will contain a disposable Node project for playing with our framework. It will serve as a lab during our development process.

    playground文件夹将包含一个用于处理我们框架的一次性Node项目。 在我们的开发过程中,它将充当实验室。

初始化Node项目 (Initializing the Node projects)

Go to the playground folder and run yarn init -y. This command generates a basic package.json file. Open it, remove the "main": "index.js", line, and add a “scripts” entry like the one in the example below:

转到playground文件夹并运行yarn init -y 。 此命令生成一个基本的package.json文件。 打开它,删除"main": "index.js",行,并添加一个“ scripts”条目,如以下示例中所示:

{  "name": "playground",  "version": "1.0.0",  "scripts": {    "test": "node tests"  },  "license": "MIT"}

After creating this file, let’s do the same for the other project, the Tyrion package itself. In the proj folder, run yarn init. It will prompt you for some information to properly create the package.json file. Enter the following values (in bold):

创建此文件后,让我们对另一个项目Tyrion包本身执行相同的操作。 在proj文件夹中,运行yarn init 。 它将提示您提供一些信息以正确创建package.json文件。 输入以下值(粗体):

question name (proj): tyrion <enter>question version (1.0.0): <enter>question description: <enter>question entry point (index.js): src/index.js <enter>question repository url: <enter>question author: <enter>question license (MIT): <enter>question private: <enter>

Now, we need to install Tyrion as a development dependency in our playground project. If it was a published package, we’d just need to install it directly, through npm i --dev or yarn add --dev. As we only have Tyrion locally, this is not possible. Luckily, both Yarn and NPM have a feature to help developers during this package “inception” phase, allowing us to simulate a link between two packages (one as a dependency of the other).

现在,我们需要在游乐场项目中安装Tyrion作为开发依赖项。 如果它是已发布的软件包,我们只需要通过npm i --devyarn add --dev直接安装yarn add --dev 。 由于我们只有本地的Tyrion,所以这是不可能的。 幸运的是,Yarn和NPM都具有在此程序包“初始”阶段帮助开发人员的功能,使我们能够模拟两个程序包之间的链接(一个作为另一个程序的依赖项)。

To create this dependency link, go to the proj folder and run:

要创建此依赖关系链接,请转到proj文件夹并运行:

$ yarn link

Then, in the playground folder, run:

然后,在playground folder ,运行:

$ yarn link tyrion

That’s all. Now Tyrion is a dependency of the playground project.

就这样。 现在提利昂是游乐场项目的依赖项。

创建一些模块作为我们的“豚鼠” (Creating some modules to be our “guinea pigs”)

In the playground/src folder, let’s create two modules to be tested by Tyrion:

playground/src文件夹中,让我们创建两个要由Tyrion测试的模块:

编写一些测试 (Writing some tests)

Now is the time to use our imagination. How should Tyrion’s DSL look? Are you sick of expect, assert , and so on? Let’s make it different, just for the fun of it. I suggest guarantee as our assertion function. Do you like it?

现在是时候发挥我们的想象力了。 Tyrion的DSL外观如何? 您是否对expectassert等等感到厌烦? 让我们变得不同,只是为了好玩。 我建议将guarantee作为我们的断言功能。 你喜欢它吗?

Let’s write a few tests to see it more clearly. Of course, nothing will work, since we didn’t implement anything in our framework.

让我们编写一些测试以更清楚地看到它。 当然,什么都行不通,因为我们没有在框架中实现任何东西。

And a tests/index.js file, to import our tests in only one place.

还有一个tests/index.js文件,可以仅在一个地方导入我们的测试。

Tyrion will borrow one of Node-tap’s principles:

提利昂将借鉴Node-tap的一项原则:

Test files should be “normal” programs that can be run directly.

测试文件应该是可以直接运行的“普通”程序。

That means that it can’t require a special runner that puts magic functions into a global space. node test.js is a perfectly ok way to run a test, and it ought to function exactly the same as when it’s run by the fancy runner with reporting and such. JavaScript tests should be JavaScript programs; not english-language poems with weird punctuation.

这意味着它不需要特殊的运行程序即可将魔术函数放到全局空间中。 node test.js是运行测试的一种很好的方法,它的功能应该与带有报告等功能的花哨运行程序运行时的功能完全相同。 JavaScript测试应该是JavaScript程序; 不是带有奇怪标点符号的英语诗歌。

https://www.node-tap.org/#tutti-i-gusti-sono-gusti.

https://www.node-tap.org/#tutti-i-gusti-sono-gusti

As you might remember, in our playground’s package.json file, we have a test script which simply runs node tests. So, to execute it, just type npm test and hit enter. Yeap, do it. Let’s see it crashing:

您可能还记得,在我们游乐场的package.json文件中,我们有一个test脚本,该脚本只运行node tests 。 因此,要执行该命令,只需键入npm test并按Enter。 是的,去做。 让我们看看它崩溃了:

This error is clear. We don’t have anything in our framework. No module is being exported at all. To fix it, in the proj folder, create a src/index.js file exporting an empty object, as you can see below:

这个错误很明显。 我们的框架中没有任何内容。 完全没有模块被导出。 要解决此问题,请在proj文件夹中创建一个src/index.js文件,该文件导出一个空对象,如下所示:

module.exports = {};

Now, we’ll run npm test again:

现在,我们将再次运行npm test

Node is complaining because our guarantee function doesn’t exist. This is simple to fix, too:

Node正在抱怨,因为我们的guarantee功能不存在。 这也很容易修复:

const guarantee = () => {};
module.exports = { guarantee };

Run the test script again:

再次运行测试脚本:

Voilà! No errors, but nothing happens, either. =(

瞧! 没有错误,但也没有任何React。 =(

担保功能 (The guarantee function)

Our assertion function should execute flawlessly if the supplied value is truthy, but should throw an error if it’s falsy.

我们断言功能应该完美执行,如果提供的值truthy ,但如果它是应该抛出一个错误falsy

Let’s implement it:

让我们实现它:

And to test if it works, let’s append another assertion to the end of our number-utils.test.js file:

为了测试它是否有效,让我们在number-utils.test.js文件的末尾附加另一个断言:

guarantee(123 === 321); // This should fail

Now run it once more:

现在再次运行它:

A-ha! It works! It’s ugly, but it’s functional.

哈! 有用! 这很丑陋,但是功能正常。

检查功能 (The check function)

We need a way to wrap assertions into test units. Basically, all testing frameworks have this feature, like the it function in Jasmine or the test function in Node-tap.

我们需要一种将断言包装到测试单元中的方法。 基本上,所有测试框架都具有此功能,例如Jasmine中的it功能或Node-tap中的test功能。

In Tyrion, our test unit function will be called check. Its signature should be check(testDescription, callback). We also want it to give us a friendlier output, describing the passing and failing tests.

在提利昂,我们的测试单元功能将称为check 。 它的签名应该是check(testDescription, callback) 。 我们还希望它给我们一个更友好的输出,描述通过和失败的测试。

This is what it will look like:

它将是这样的:

Now, we can rewrite our tests to use the new check function:

现在,我们可以重写测试以使用新的check功能:

And re-run our test suite:

并重新运行我们的测试套件:

Cool. But… what about some colors?? Wouldn’t it be a lot easier to distinguish between passing and failing tests?

凉。 但是……那有些颜色呢? 区分通过和不通过的测试会容易得多吗?

Add the colors module as a dependency:

添加colors模块作为依赖项:

yarn add colors

So, import it at the top of the proj/src/index.js file:

因此,将其导入到proj/src/index.js文件的顶部:

const colors = require('colors');

And let’s put some colors in our output:

让我们在输出中添加一些颜色:

const check = (title, cb) => {  try{    cb();    console.log(`${' OK '.bgGreen.black} ${title.green}`);  } catch(e) {    console.log(`${' FAIL '.bgRed.black} ${title.red}`);    console.log(e.stack.red);  }};

That’s better. =)

这样更好 =)

xcheck功能 (The xcheck function)

It would be nice to have an easy way to disable a specific test, like the xit function in Jasmine. This can be easily implemented by creating a no-op function which just outputs that a test is disabled (well, it’s not completely no-op, but almost):

有一种简单的方法来禁用特定测试会很好,例如Jasmine中的xit函数。 这可以通过创建一个无操作功能来轻松实现,该功能仅输出禁用了测试的功能(嗯,它不是完全无操作的,而是几乎):

const xcheck = (title, cb) => {  console.log(`${' DISABLED '.bgWhite.black} ${title.gray}`);};
module.exports = { guarantee, check, xcheck };

So, import the xcheck function in the number-utils.test.js file and disable one of our tests:

因此,将xcheck函数导入number-utils.test.js文件并禁用我们的测试之一:

const { guarantee, check, xcheck } = require('tyrion');const numberUtils = require('../src/number-utils');
// method: isPrimexcheck('returns true for prime numbers', () => {  guarantee(numberUtils.isPrime(2));  guarantee(numberUtils.isPrime(3));  guarantee(numberUtils.isPrime(5));  guarantee(numberUtils.isPrime(7));  guarantee(numberUtils.isPrime(23));});

And here’s how it behaves:

这是它的行为方式:

测试摘要和退出代码 (Test summary and exit code)

If we wanted to use Tyrion in a CI server, it would need to finish its process with different exit codes for error and success conditions.

如果我们想在CI服务器中使用Tyrion,则需要为错误和成功条件使用不同的退出代码完成其过程。

Another desirable feature is a test summary. It would be nice to know how many tests passed, failed, or skipped (the disabled ones). For this, we could increment some counters in both check and xcheck functions.

另一个理想的功能是测试摘要。 很高兴知道有多少测试通过,失败或跳过(禁用的测试)。 为此,我们可以在checkxcheck函数中增加一些计数器。

We will create the end function, which prints the test summary and finishes with the appropriate exit code:

我们将创建end函数,该函数将打印测试摘要并以适当的退出代码结束:

And don’t forget to call it in the playground/tests/index.js file:

并且不要忘记在playground/tests/index.js文件中调用它:

const { end } = require('tyrion');
require('./string-utils.test');require('./number-utils.test');
end();

Or maybe:

或许:

const tyrion = require('tyrion');
require('./string-utils.test');require('./number-utils.test');
tyrion.end();

Now, let’s re-run npm test:

现在,让我们重新运行npm test

Great, it works.

太好了,它有效。

分组功能 (The group function)

Many test frameworks have some way of grouping related tests. In Jasmine, for example, there is the describe function. We will implement a group function for this purpose:

许多测试框架都有一些将相关测试分组的方法。 例如,在Jasmine中,有describe函数。 为此,我们将实现group功能:

And update our tests to use this new function:

并更新测试以使用此新功能:

Here’s the new output:

这是新的输出:

Well, the good news is that it works. The bad news is that it’s getting hard to understand. We need a way to indent this output in order to make it more readable:

好吧,好消息是它有效。 坏消息是,它变得越来越难以理解。 我们需要一种缩进此输出以使其更具可读性的方法:

Run it again:

再次运行:

That’s way better!

这样更好!

So, how does it work?

那么它是怎样工作的?

  • The repeat function repeats a string n times.

    repeat函数将字符串重复n次。

  • The indent function repeats an indent (of four spaces) n times by using the repeat function.

    indent功能通过使用repeat功能重复缩进n次(四个空格) n次。

  • The indentLines function indents a string with multiple lines by adding n indents to the beginning of each line. We’re using it to indent error stacks.

    indentLines函数通过在每行的开头添加n缩进来缩进具有多行的字符串。 我们正在使用它缩进错误堆栈。

  • The indentLevel variable is incremented at the beginning of each group execution and decremented at its end. This way, nested groups can be correctly indented.

    indentLevel变量在每个组执行开始时增加,并在其结束时减少。 这样,嵌套组可以正确缩进。

更多匹配者 (More matchers)

The guarantee function is not flexible enough for a lot of scenarios. We need a richer set of matchers in order to make our tests more meaningful.

guarantee功能在很多情况下不够灵活。 我们需要一套更丰富的匹配器,以使我们的测试更有意义。

First, create the matchers folder:

首先,创建matchers文件夹:

$ mkdir proj/src/matchers

Now, we’ll create each matcher in a separate file:

现在,我们将在单独的文件中创建每个匹配器:

The same matcher uses the strict equality operator (===) to test if two arguments are exactly the same object (for reference types) or equal (for primitive types). It behaves similarly to the toBe matcher in Jasmine and t.equal in node-tap.

same匹配器使用严格相等运算符(===)来测试两个参数是完全相同的对象(对于引用类型)还是相等(对于原始类型)。 它的行为类似于Jasmine中的toBe匹配器和node-tap中的t.equal

Note: Node-tap also has a matcher called t.same, but it works differently (it won’t verify if two objects are exactly the same, but if both are deeply equivalent).

注意: Node-tap也有一个名为t.same的匹配器,但它的工作方式不同(它不会验证两个对象是否完全相同,但是否都完全相同)。

The identical matcher verifies that two arguments are equivalent. It uses the == operator for comparing values.

identical匹配器将验证两个参数是否相等。 它使用==运算符比较值。

The deeplyIdentical matcher does a deep comparison of two objects. This kind of comparison can be considerably complex, or at least too complex for this article’s purpose. So, let’s install an existing module to handle deep equality and use it in our matcher:

deeplyIdentical匹配器对两个对象进行了深度比较。 这种比较可能非常复杂,或者至少对于本文的目的而言过于复杂。 因此,让我们安装一个现有模块来处理深度相等并在我们的匹配器中使用它:

$ yarn add deep-equal

Then:

然后:

This is how an error will look:

这是错误的样子:

The falsy matcher will fail if the supplied value is truthy.

falsy如果提供的是truthy匹配将失败。

The truthy matcher works in a similar manner to our guarantee function. It passes when the supplied value is truthy and breaks if it’s falsy.

truthy匹配器的工作方式与我们的guarantee功能类似。 当提供的值是真值时,它将通过;如果提供的值是假值,则将中断。

The throws matcher will pass if a function throws an error. It’s possible to specify the wanted error message, but this is not mandatory.

如果函数抛出错误,则throws匹配器将通过。 可以指定所需的错误消息,但这不是强制性的。

An index.js file to re-export all matchers:

一个index.js文件,用于重新导出所有匹配器:

And finally let’s glue them all together:

最后,让我们将它们粘合在一起:

You can use our new matchers this way:

您可以通过以下方式使用我们的新匹配器:

const { guarantee, check } = require('tyrion');
check('playing with our new matchers', () => {  // The original guarantee function still works  guarantee(123 === 123);
guarantee.truthy('abc');  guarantee.falsy(null);
const a = { whatever: 777 };  const b = a;  guarantee.same(a, b);  guarantee.identical(undefined, null);
const c = { whatever: { foo: { bar: 'baz' } } };  const d = Object.assign({}, c);  guarantee.deeplyIdentical(c, d);
function boom() { throw new Error('Some error...'); }  guarantee.throws(boom);  guarantee.throws(boom, 'Some error...');});

beforeEach函数 (The beforeEach function)

To implement a beforeEach function, we need to use a stack to accumulate all beforEach callbacks. This is done for each new scoped level created every time a group is declared:

要实现beforeEach函数,我们需要使用堆栈来累积所有beforEach回调。 每次在声明组时创建的每个新作用域级别都会完成此操作:

How does it work?

它是如何工作的?

  • Every time a group is declared, we’re pushing a new array to the beforeEachStack variable. This array will accumulate all beforeEach callbacks declared in that scope.

    每次声明一个组时,我们都会将一个新数组推入beforeEachStack变量。 该数组将累积该作用域中声明的所有beforeEach回调。

  • After a group execution is completed, we remove the array at the top of our callbacks stack.

    组执行完成后,我们在回调堆栈的顶部删除数组。
  • The beforeEach function receives a callback and appends it to the array at the top of our callbacks stack.

    beforeEach函数接收一个回调并将其追加到回调堆栈顶部的数组中。

  • At the beginning of each check function, we’re calling every beforeEach callback in all levels of our stack.

    在每个check功能的开始,我们在堆栈的所有级别中调用每个beforeEach回调。

beforeAll函数 (The beforeAll function)

Our last addition will be the beforeAll function. For simplicity’s sake, we’re assuming that calls to the beforeAll function will always be put before all groups and tests (or, when scoped within a group, at its very top).

我们的最后一个添加将是beforeAll函数。 为简单起见 ,我们假设对beforeAll函数的调用将始终放在所有组和测试之前( ,在组内作用域时,位于其顶部)。

Otherwise, if we wanted to ensure that the beforeAll function works correctly even in the middle or at the end of a group, we should dramatically change our existing logic. Well, we’re not going to do that, since it isn’t a rational usage of this function.

否则,如果我们要确保beforeAll函数即使在组的中间或末尾也beforeAll正常工作,则应大幅度更改现有逻辑。 好吧,我们不会这样做,因为这不是此功能的合理用法。

Our version of beforeAll will just receive a callback and immediately execute it.

我们的beforeAll版本将只收到一个回调并立即执行它。

const beforeAll = cb => cb();
module.exports = {   group, check, xcheck, guarantee, beforeAll, end };

An example of usage:

用法示例:

const { guarantee, check, group, beforeAll } = require('tyrion');
let a;beforeAll(() => {  a = { something: 'example' };});
group('playing with the beforeAll function', () => {  let b;  beforeAll(() => {    b = { something: 'example' };  });
check('some test', () => {    guarantee.deeplyIdentical(a, b);  });
check('another test', () => {    guarantee.identical(11, 11);  });});

提利昂的最终版本 (The final version of Tyrion)

It has been a long journey, but Tyrion is finally complete. =)

这是一段漫长的旅程,但是提利昂终于完成了。 =)

I added a SILENT option which disables logging. It’s being used to make it easier to test Tyrion (yep, testing frameworks need to be tested too).

我添加了一个SILENT选项来禁用日志记录。 它用于简化Tyrion的测试(是的,测试框架也需要测试)。

The complete project is available here.

完整的项目在这里

可能的改进 (Possible improvements)

Tyrion lacks many features, like:

Tyrion缺少许多功能,例如:

  • Support for async tests

    支持异步测试
  • Parallel execution of tests

    并行执行测试
  • afterEach and afterAll functions

    afterEachafterAll函数

  • A xgroup function, which disables an entire group

    xgroup函数,它禁用整个组

  • A function similar to Jasmine’s fit

    类似于茉莉花的健康功能

  • Spies

    间谍
  • Decoupling DSL from reporting logic.

    将DSL与报告逻辑解耦。
  • Pluggable reporters

    可插拔的记者
  • A terminal CLI (with a --watch option)

    终端CLI(带有--watch选项)

  • Yet more matchers

    更多匹配者
  • Friendlier error stacks

    友好的错误堆栈

I encourage you to keep playing with this project. Feel free to use and expand it. Please let me know your thoughts, suggestions, and experiments by leaving a comment below. =)

我鼓励您继续参与这个项目。 随时使用和扩展它。 请在下面留下评论,让我知道您的想法,建议和实验。 =)

翻译自: https://www.freecodecamp.org/news/learnbydiy-how-to-create-a-javascript-unit-testing-framework-from-scratch-c94e0ba1c57a/

从头搭建rpc框架

 类似资料: