首頁  >  文章  >  web前端  >  JS單元測驗講座-實用指南

JS單元測驗講座-實用指南

王林
王林原創
2024-08-30 19:00:391010瀏覽

這是 Maximilian Schwarzmüller 講授的《JavaScript 單元測試 - 實用指南》課程的總結。


介紹

深入研究自動化測試可能是一個好主意,因為與手動測試相比,它更具可預測性和一致性。

讓我們考慮一個場景,您新增功能或更改程式碼的某些部分。

您不一定知道或意識到程式碼中真正受到影響的每個部分或所有部分。

通過手動測試,我們必須測試(或嘗試)整個應用程序,或至少測試可能受更改影響的部分。為什麼?因為我們需要看看一切是否仍然正常運作,而那個微小的變化或新功能沒有破壞某處的程式碼。

所以,正如我們想像的那樣,有很多工作

此外,不能保證需要測試的所有內容都會被測試,並且每次發生變化時,都不能保證以相同的方式進行測試。

透過自動化測試,我們有初步的努力,但後來會帶來很多好處。

在當今我們擁有的各種自動化測試中,這裡我們將討論單元測試和整合測試,但主要是單元測試。

讓我們將單元視為程式碼的最小部分,例如函數或類別。因此,在單元測試中,顧名思義,我們將測試應用程式的每個單元。因此,如果我們有 50 個函數,我們將為所有或大多數函數建立測試。這是因為我們希望確保每個部分、每個單元都按照最初的預期正常運作。

另一方面,整合測試將關註一起測試這些單元,或者更好地說,它們如何一起工作以及它們是否可以很好地一起工作。這是因為即使我們單獨測試這些單元,也不能保證它們能夠協同工作或按應有的方式工作。

測試驅動開發 (TDD)

我們也應該了解 TDD,也就是測試驅動開發的簡稱。

TDD 是一個框架/哲學,它引導我們考慮先寫失敗的測試,然後實現使測試成功的程式碼。然後重構,作為一個循環的事情。

一個提醒

讓你的測驗保持簡單!

當其他人需要閱讀你的程式碼,甚至在未來的場景中需要閱讀你的程式碼時,重要的是不要花太長時間來理解。這必須是一項簡單的任務。

維泰斯特

對於本指南,我們將使用 Vitest,這是一個基於著名的 Jest 應用程式的工具,用於測試等。在這裡,它不是要深入 Vitest 語法或所有功能,而是作為理解測試核心的工具。

如果您想了解或查看 Vitest 可以幫助我們的所有內容,請訪問以下連結的文檔:Vitest 文件

暗示

Vitest 的工作方式與 Webpack 捆綁工具類似,因此在使用 ES 模組表示法時,我們實際上不需要明確告知要導入的檔案的副檔名。例如:

從“./math/Math”導入數學

相反的是:

從 './math/Math.js' 匯入數學


良好做法

這裡有一個小指南,可引導您在編寫測試例程中進行良好實踐。

編寫好的測試

單元和整合測試可能非常有用,但前提是寫得好。為此,我們可以遵循以下將探討的一系列「良好實踐」:

只測試你的程式碼

當我們談論測試我們自己的程式碼時,這意味著第三方程式碼不是我們測試的責任。嗯,它是供第三方程式碼編寫者使用的,以確保其正常工作,因此測試它是他們的責任。

而且因為你無法改變它,所以測試它沒有用。

了解如何區分測試

您不會透過客戶端程式碼隱式測試伺服器端程式碼。

您將測試客戶端對不同回應和錯誤的反應。

因此,將您的測試、前端開發測試和後端開發測試分開。

AAA

  • 安排:定義測驗環境與值
  • Act:執行應該測試的實際程式碼/函數
  • 斷言:評估產生的值/結果並與預期值/結果進行比較。

這裡有一個 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.

Essence of what is being tested

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.

Test one thing

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:

Lectures of JS Unit Testing - The Practical Guide

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.

SO,

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.

Code coverage

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


Callbacks and Async Functions

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.

Try and catch

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.

Promises

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).


Hooks, spies and mocks

Here, we are going to explore a little bit of these important functionalities that Vitest provides to us.

Hooks

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

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.


Conclusion

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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn