막시밀리안 슈바르츠뮐러 선생님이 강의한 "JavaScript Unit Testing - The Practical Guide" 강의 요약입니다.
자동 테스트를 활용하는 것은 수동 테스트에 비해 훨씬 더 예측 가능하고 일관성이 있기 때문에 좋은 아이디어가 될 수 있습니다.
새로운 기능을 추가하거나 코드의 일부를 변경하는 시나리오를 생각해 보겠습니다.
진짜 영향을 받은 코드의 각 부분 또는 전체 부분을 반드시 알거나 인식할 필요는 없습니다.
수동 테스트에서는 전체 애플리케이션을 테스트(또는 시도)해야 하거나 최소한 변경으로 인해 영향을 받을 수 있는 항목을 테스트해야 합니다. 왜? 모든 것이 여전히 작동하는지 확인해야 하고 작은 변경이나 새로운 기능으로 인해 코드가 손상되지 않았는지 확인해야 하기 때문입니다.
그래서 우리가 상상할 수 있듯이 할 일이 많습니다.
또한 테스트해야 할 모든 항목이 테스트된다는 보장도 없고, 변경사항이 있을 때마다 동일한 방식으로 테스트된다는 보장도 없습니다.
자동 테스트를 사용하면 초기에는 노력이 필요하지만 나중에 많은 이점을 얻을 수 있습니다.
요즘 다양한 자동화 테스트 중에서 단위 테스트와 통합 테스트에 대해 이야기하겠습니다. 하지만 주로 단위 테스트에 대해 설명하겠습니다.
함수나 클래스와 같이 코드의 가장 작은 부분인 유닛을 생각해 봅시다. 따라서 단위 테스트에서는 이름에서 알 수 있듯이 애플리케이션의 각 단위를 테스트합니다. 따라서 예를 들어 50개의 함수가 있는 경우 해당 함수 모두 또는 대다수에 대한 테스트를 생성합니다. 그 이유는 각 부품, 각 단위가 처음 의도한 대로 제대로 작동하는지 보장하고 싶기 때문입니다.
반면에 통합 테스트는 이러한 단위를 함께 테스트하는 것, 더 잘 말하면 이들이 어떻게 함께 작동하는지, 그리고 서로 잘 작동하는지에 대해 관심을 갖습니다. 유닛을 단독으로 테스트하더라도 유닛이 함께 작동하거나 제대로 작동한다는 보장은 없기 때문입니다.
Test-Driven Development의 약어인 TDD에 대해서도 알아두어야 합니다.
TDD는 실패한 테스트를 먼저 작성하고 테스트를 성공시킬 코드를 구현하는 것에 대해 생각하게 하는 프레임워크/철학입니다. 그런 다음 주기적으로 리팩터링합니다.
테스트를 단순하게 유지하세요!
다른 사람이 귀하의 코드를 읽어야 하거나 미래의 시나리오에서 귀하가 읽어야 하는 경우 이해하는 데 너무 오래 걸리지 않는 것이 중요합니다. 쉬운 작업이어야 합니다.
이 지침을 위해 우리는 테스트 등을 위한 유명한 Jest 앱을 기반으로 한 도구인 Vitest를 사용할 예정입니다. 여기서는 Vitest 구문이나 모든 기능에 대해 자세히 알아보기 위한 것이 아니라 테스트의 핵심을 이해하기 위한 도구입니다.
Vitest가 우리에게 도움을 줄 수 있는 모든 것을 배우고 보려면 다음 링크의 문서로 이동하십시오: Vitest 문서
Vitest는 번들링을 위한 Webpack 도구와 유사하게 작동할 수 있으므로 ES 모듈 표기법을 사용할 때 실제로 가져오는 파일의 확장자를 명시적으로 알릴 필요가 없습니다. 예:
'./math/Math'에서 수학 가져오기
그 반대는 다음과 같습니다.
'./math/Math.js'에서 수학 가져오기
다음은 작문 테스트 루틴에서 좋은 습관을 들이는 데 도움이 되는 작은 가이드입니다.
단위 및 통합 테스트는 매우 유용할 수 있지만 잘 작성된 경우에만 가능합니다. 이를 위해 아래에서 살펴볼 일련의 "우수 사례"를 따를 수 있습니다.
자체 코드를 테스트한다는 것은 타사 코드가 테스트할 책임이 없다는 의미입니다. 글쎄요, 제3자 코드가 제대로 작동하는지 확인하기 위해 제3자 코드를 작성한 사람을 위한 것이므로 테스트하는 것은 그 사람의 책임입니다.
또한 변경할 수 없기 때문에 테스트해봐도 소용이 없습니다.
클라이언트측 코드를 통해 암시적으로 서버측 코드를 테스트하지는 않습니다.
그리고 다양한 응답과 오류에 대한 클라이언트측 반응을 테스트하게 됩니다.
따라서 테스트, 프런트엔드 개발용 테스트, 백엔드 개발용 테스트를 분리하세요.
다음은 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"을 import하고, 예제용으로 빌드된 함수이지만 다른 파일에 있는 "toSum" 함수를 import합니다.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!