これは、Maximilian Schwarzmüller が講義した「JavaScript 単体テスト - 実践ガイド」コースの概要です。
自動テストに取り組むことは、手動テストに比べてはるかに予測可能で一貫性があるため、素晴らしいアイデアになる可能性があります。
新しい機能を追加するか、コードの一部を変更するシナリオを考えてみましょう。
実際に影響を受けたコードの各部分またはすべての部分を必ずしも知っているわけではありません。
手動テストでは、アプリケーション全体、または少なくとも変更の影響を受ける可能性のあるものをテスト (または試行) する必要があります。なぜ?なぜなら、すべてがまだ動作しているかどうか、そしてその小さな変更や新機能がどこかでコードを壊していないかどうかを確認する必要があるからです。
ですから、ご想像のとおり、大変な作業です。
また、テストする必要があるすべてがテストされるという保証はありません。また、何かが変更されるたびに、同じ方法でテストされるという保証もありません。
自動テストを使用すると、最初は手間がかかりますが、後で多くのメリットがもたらされます。
今日ではさまざまな自動テストがありますが、ここでは単体テストと統合テストについて説明しますが、主に単体テストについて説明します。
ユニットとは、関数やクラスなど、コードの最小部分であると考えてみましょう。したがって、単体テストでは、名前が示すように、アプリケーションの各ユニットをテストします。したがって、たとえば 50 個の関数がある場合、そのすべてまたは大部分に対するテストを作成します。それは、各部品、各ユニットが当初の意図どおりに正常に動作することを保証したいからです。
一方、統合テストでは、これらのユニットを一緒にテストするか、より正確に言えば、それらがどのように連携するか、連携してうまく動作するかどうかが考慮されます。それは、たとえユニットを単独でテストしたとしても、それらが連携して動作していること、または正常に動作していることを保証するものではないからです。
TDD (テスト駆動開発の略称) についても認識する必要があります。
TDD は、最初に失敗するテストを作成し、次にテストを成功させるコードを実装することを考えるように導くフレームワーク/哲学です。そして、循環的なものとしてリファクタリングを行います。
テストはシンプルにしてください!
他の人があなたのコードを読む必要がある場合、あるいは将来のシナリオであなた自身がコードを読む必要がある場合、理解するのにそれほど時間がかからないことが重要です。それは簡単な作業である必要があります。
このガイダンスでは、有名な Jest アプリをベースにしたテストなどのツールである Vitest を使用します。ここでは、Vitest 構文やすべての機能を深く掘り下げることを目的としたものではなく、テストの中核を理解するためのツールとして説明します。
Vitest がサポートするすべてのことを知りたい、または確認したい場合は、次のリンクにあるドキュメントにアクセスしてください: Vitest ドキュメント
Vitest はバンドル用の Webpack ツールと同様に機能するため、ES モジュール表記を使用する場合、実際にはインポートするファイルの拡張子を明示的に通知する必要はありません。例:
「./math/Math」から数学をインポートします
その逆は次のとおりです:
「./math/Math.js」から数学をインポートします
ここでは、ライティング テスト ルーチンの優れた実践方法を紹介するための小さなガイドを示します。
単体テストと統合テストは非常に役立ちますが、それは適切に記述されている場合に限られます。そのためには、以下で説明する一連の「グッドプラクティス」に従うことができます。
私たちが独自のコードのテストについて話すとき、それはサードパーティのコードが私たちにテストする責任がないことを意味します。まあ、これはサードパーティのコードを書いた人が正常に動作することを確認することを目的としているので、テストするのはその人の責任です。
また、変更できないため、テストしても役に立ちません。
クライアント側のコードを介してサーバー側のコードを暗黙的にテストすることはありません。
そして、さまざまな応答やエラーに対するクライアント側の反応をテストします。
そのため、フロントエンド開発用のテストとバックエンド開発用のテストを分けてください。
ここに Vitest コード例があります:
import { expect, it } from 'vitest'; import { toSum } from './math'; it('should sum two numbers', () => { //Arrange const num1 = 1; const num2 = 2; //Act const result = toSum(num1, num2); //Assert const expectedResult = num1 + num2; expect(result).toBe(expectedResult); });
あなたが気づいていない場合に備えて、コードが何をしているのかを明確にするためです。
まず、必要な機能である vitest から "expect" と "it" をインポートします。また、この例用に構築された関数ですが、別のファイルにある "toSum" 関数をインポートします。
The "it" works as the scope for our test; it receives a string that behaves as the identifier and a function that will run the test code. Here is very simple; we are saying that it should sum two numbers, that's our expectation for the function and for the test.
In the arrange part we create the variables which will be passed to the "toSum" function. Then, in the act part, we create a constant that will receive the result of the function. Finally, in the assert, we will store the expected result, which would be the sum of the two numbers and use "expect" from Vitest to make the assertion. Basically, we are saying that we expect the result to be the same as the expected result.
There are many assertion possibilities, so please do check the documentation for further study.
Note: "it" is an alias for "test"
Also, it's very important the following line:
const expectedResult = num1 + num2;
Imagine if we've done it like this:
const expectedResult = 3;
It's okay for the current test because we are indeed expecting 3 as the result.
But, imagine in the future, someone changes "num1" or "num2", and forgets to change the "expectedResult"; it would not work if the result of the sum was not 3.
If you, for example, created a function that is going to receive an array of numbers as an argument and you need to test if it actually received an array of numbers in your test file, you just need to reference an array, for example:
const items = [1, 2];
You don't need to create a bigger array, for example:
const items = [1, 2, 3, 4, 5, 6, 7, 8];
It's unnecessary and redundant. Keep it short, simple and concise, so you will make sure that you are only testing what needs to be tested or what is important for the function.
You can think of one thing as one feature or one behavior. For example, if you have a function that will sum two numbers (the same example above) and you need to make sure that is summing two numbers (because indeed that's the main functionality) but also that the output is of type number, then, you can separate it into two assertions, for example:
import { describe, expect, it } from 'vitest'; import { toSum } from './math'; describe('toSum()', () => { it('should sum two numbers', () => { const num1 = 1; const num2 = 2; const result = toSum(num1, num2); const expectedResult = num1 + num2; expect(result).toBe(expectedResult); }); it('should output a result of type number', () => { const num1 = 1; const num2 = 2; const result = toSum(num1, num2); const expectedResult = num1 + num2; expect(result).toBe(expectedResult); }); })
If you're wondering what describe does, it help us to create suites. As many suites as we want, like dividing blocks of tests. It keeps our code organized, clear, and easier to understand the outputting.
Here's an example using the toSum function:
As you can see in the image above, it will show us the file path, and after that the "describe" name, and then the "it" name. It's a good idea to keep the describer name short and referencing the function to what the tests are about.
And you could have describers inside describers to organize even further, it's up to you.
when we create our tests following good practices, we are creating tests that will actually help us on what's needed to be tested. And also, testing forces us to write better code. For that, we can write good functions that will hold only the logic of that function so it'll be easier to test what's need to be tested.
It's important to understand also that coverage doesn't mean good testing or testing that is useful and meaningful for the application. Well, you could cover 100% of your code with meaningless tests after all, or, missing important tests that you didn't think of.
Don't see a high amount of code coverage as the ultimate goal!
You will want to try and test cover the majority of the units (functions or classes) in your application, because that's what unit testing is about, but, there is some code that doesn't need to be tested.
Vitest comes with a built-in functionality to help us measure the code coverage; you can access in the following link: Vitest coverage tool
As callbacks and async functions exhibit specific behavior in Vitest, this section is dedicated to exploring them superficially.
When testing for a callback, keep in mind that Vitest does not wait for the response or for the callback to be executed. Therefore, we need to use the "done" argument.
Consider the following test as an example:
import { expect, it } from 'vitest'; import { generateToken } from './async-example'; it('should generate a token value', (done) => { const email = 'test@mail.com'; generateToken(email, (err, token) => { expect(token).toBeDefined(); done() }) })
Now, we are working with a callback function. Notice that there's a parameter being passed. The "done".
Vitest now needs to wait until the done function is called.
What would happen if we didn't use the "done" argument? The "expect" wouldn't be executed.
Still in that function, imagine if we changed toBeDefined to toBe, as in the image below:
import { expect, it } from 'vitest'; import { generateToken } from './async-example'; it('should generate a token value', (done) => { const email = 'test@mail.com'; generateToken(email, (err, token) => { expect(token).toBe(2); done(); }); })
By default, in Vitest, the "toBe" function throws an error each time something doesn't go as expected, in this case, if the token returned wasn't 2.
However, as we are working with a callback, we will need to add an exception handling syntax, such as "try and catch", because if we don't do so, the test will timeout.
import { expect, it } from 'vitest'; import { generateToken } from './async-example'; it('should generate a token value', (done) => { const email = 'test@mail.com'; try { generateToken(email, (err, token) => { expect(token).toBe(2); }); } catch (error) { done(error); } })
Since we are dealing with an error, we also need to pass this error to the "done" function.
Now, when working with promises, it's a bit easier, or should we say, simpler.
import { expect, it } from 'vitest'; import { generateTokenPromise } from './async-example'; it('should generate a token value', () => { const email = 'test@mail.com'; return expect(generateTokenPromise(email)).resolves.toBeDefined(); //or return expect(generateTokenPromise(email)).rejects.toBe(); });
Here we have two possibilities: resolves and rejects
The "return" statement guarantees Vitest waits for the promise to be resolved.
Alternatively, we have:
import { expect, it } from 'vitest'; import { generateTokenPromise } from './async-example'; it('should generate a token value', async () => { const email = 'test@mail.com'; const token = await generateTokenPromise(email); expect(token).resolves.toBeDefined(); // or expect(token).rejects.toBe(); })
Here we don't need to use "return" because "async/await" is being used (since a function annotated with "async" returns a promise implicitly).
Here, we are going to explore a little bit of these important functionalities that Vitest provides to us.
Imagine working with a bunch of tests that use the same variable, and you don't want to initialize it every single time in every single test because it's the same.
Hooks can help us in this case because you can use functions provided by it that allow us to reuse this variable.
Functions available: "beforeAll", "beforeEach", "afterEach", "afterAll".
Here goes a simple example just to show how it works:
import { beforeEach } from 'vitest'; let myVariable; beforeEach(() => { myVariable = ""; }); it('sentence', () => { myVariable = "Hello"; }); it('another sentence', () => { myVariable += 2; });
Now, imagine the same but without the hook:
let myVariable; it('sentence', () => { myVariable = "Hello"; }); it('another sentence', () => { myVariable += 2; });
As we can see, when using "beforeEach" and a global variable, before each test starts to execute, the variable will be "cleaned". This allows the tests to use the variable as if it were fresh.
But, without using the hook and using a global variable, in some cases, things would be tricky. In the example of the test "another sentence," if we didn't clean the variable, it would be holding "Hello" because the "sentence" test is run first. And that's not what we want.
Mocks and spies are mainly to handle side effects and external dependencies.
We could say that spies help us deal with side effects in our functions, and mocks help us deal with side effects of external dependencies.
For that, you will have to import "vi" from vitest.
To build a spy, you can use "vi.fn()" and for a mock "vi.mock()". Inside each function, you will pass the name to the other function (your or external).
So, spies and mocks kind of replace the actual functions with other functions or empty functions.
Mocks will be available only for tests of the file you called them and Vitest, behind the scenes, puts them at the start of the file.
In summary, you need to consider what the unit should or should not do. To achieve this, you can utilize the "it" syntax provided by Vitest, which takes a string describing your expectations and a function that will test the given expectations.
The name of the test should be short, simple and easy to understand.
The testing magic lies in thinking about aspects that were not initially considered, leading to code improvement. This process helps prevent errors and promotes a clearer understanding of what the function should do and its expected behaviors.
以上がJS単体テスト講座 - 実践ガイドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。