애플리케이션이 Azure 서비스와 올바르게 상호 작용하도록 하려면 JavaScript용 Azure SDK에 대한 통합 코드를 테스트해야 합니다. 이 가이드에서는 JavaScript 애플리케이션에서 테스트 프레임워크에서 Azure SDK 통합을 효과적으로 테스트하는 방법을 보여 줍니다.
클라우드 서비스 SDK 호출을 모의할지 아니면 테스트 목적으로 라이브 서비스를 사용할지 결정할 때 속도, 안정성 및 비용 간의 장단점이 고려되어야 합니다. 이 문서에서는 테스트 프레임워크를 사용하여 SDK 통합을 테스트하는 방법을 보여 줍니다. 애플리케이션 코드는 Cosmos DB에 문서를 삽입합니다. 테스트 코드는 클라우드 리소스가 사용되지 않도록 해당 리소스 사용을 조롱합니다.
사용되는 프레임워크는 다음과 같습니다.
- CommonJs를 사용하는 Jest
- Vitest와 ESM
- ESM을 사용하여 테스트 실행기 Node.js
필수 조건
Node.js LTS LTS 릴리스 상태는 일반적으로 총 30개월 동안 중요한 버그가 수정되도록 보장하는 "장기 지원"입니다.
Node.js 테스트 러너는 Node.js 설치의 일부입니다.
주의
Node.js 테스트 실행기용으로 제공된 샘플은 mock.fn()과 함께 실험적 node:test 모듈을 사용합니다. Node의 기본 제공 테스트 실행기는 아직 완전히 지원되는 모의 API를 제공하지 않습니다. 대상 노드 버전이 실험적 API를 지원하는지 확인하거나 타사 모의 라이브러리(또는 스텁 함수)를 대신 사용하는 것이 좋습니다.
클라우드 서비스 모의
장점:
- 네트워크 대기 시간을 제거하여 테스트 제품군의 속도를 향상합니다.
- 예측 가능하고 제어된 테스트 환경을 제공합니다.
- 다양한 시나리오 및 에지 사례를 보다 쉽게 시뮬레이션할 수 있습니다.
- 특히 연속 통합 파이프라인에서 라이브 클라우드 서비스 사용과 관련된 비용을 줄입니다.
단점:
- 모의 데이터가 실제 SDK와 차이가 생기면 불일치를 일으킬 수 있습니다.
- 라이브 서비스의 특정 기능 또는 동작을 무시할 수 있습니다.
- 프로덕션에 비해 덜 현실적인 환경입니다.
라이브 서비스 사용
장점:
- 프로덕션을 밀접하게 반영하는 현실적인 환경인가요?
- 통합 테스트에서 시스템의 여러 부분이 함께 작동하도록 하는 데 유용합니까?
- 네트워크 안정성, 서비스 가용성 및 실제 데이터 처리와 관련된 문제를 식별하는 데 도움이 합니까?
단점:
- 네트워크 호출로 인해 속도가 느립니다.
- 잠재적인 서비스 사용 비용으로 인해 비용이 더 많이 듭니다.
- 프로덕션과 일치하는 라이브 서비스 환경을 설정하고 유지 관리하는 데 복잡하고 시간이 많이 걸립니다.
모의 및 라이브 서비스 사용 중에서 선택하는 것은 테스트 전략에 따라 달라집니다. 속도와 제어가 가장 중요한 단위 테스트의 경우 모의 작업이 더 나은 선택인 경우가 많습니다. 리얼리즘이 중요한 통합 테스트의 경우 라이브 서비스를 사용하면 보다 정확한 결과를 제공할 수 있습니다. 이러한 접근 방식의 균형을 맞추면 비용을 관리하고 테스트 효율성을 유지하면서 포괄적인 테스트 범위를 달성할 수 있습니다.
테스트 더블: 모의, 스텁 및 가짜
테스트 더블은 테스트 목적으로 실제 항목 대신 사용되는 모든 종류의 대체품입니다. 선택하는 double 유형은 그것이 대체하고자 하는 항목에 기반합니다.
모의라는 용어는 용어가 비공식적으로 사용될 때 종종 이중으로 뜻합니다. 이 문서에서는 이 용어가 Jest 테스트 프레임워크에서 구체적으로 사용되고 구체적으로 설명되어 있습니다.
Mock(모의)
모의( 스파이라고도 함): 함수를 대체하고 다른 코드에서 간접적으로 호출될 때 해당 함수의 동작 을 제어하고 감시할 수 있습니다.
다음 예제에는 2개의 함수가 있습니다.
-
someTestFunction: 테스트해야 하는 함수입니다. 종속성을 호출합니다. 이 종속성은
dependencyFunction
작성하지 않았으며 테스트할 필요가 없습니다.
-
dependencyFunctionMock: 종속성의 모의입니다.
import { mock } from 'node:test';
import assert from 'node:assert';
// ARRANGE
const dependencyFunctionMock = mock.fn();
// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
assert.strictEqual(dependencyFunctionMock.mock.callCount(), 1);
// ARRANGE
const dependencyFunctionMock = jest.fn();
// ACT
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
expect(dependencyFunctionMock).toHaveBeenCalled();
import { expect, vi } from 'vitest';
// ARRANGE
const dependencyFunctionMock = vi.fn();
// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
expect(dependencyFunctionMock).toHaveBeenCalledTimes(1);
테스트의 목적은 실제로 종속성 코드를 호출하지 않고 someTestFunction이 올바르게 작동하는지 확인하는 것입니다. 이 테스트는 종속성의 모의 개체가 호출되었는지 확인합니다.
큰 종속성과 작은 종속성 모의
종속성을 모의하기로 결정한 경우 다음과 같이 필요한 항목만 모의하도록 선택할 수 있습니다.
- 더 큰 종속성에서 하나 또는 두 개의 함수입니다. Jest는 이 목적을 위해 부분 모의를 제공합니다.
- 이 문서의 예제와 같이 더 작은 종속성의 모든 함수입니다.
스텁
스텁의 목적은 함수의 반환 데이터를 대체하여 다양한 시나리오를 시뮬레이션하는 것입니다. 스텁을 사용하여 코드가 함수를 호출하고 성공적인 결과, 실패, 예외 및 에지 사례를 비롯한 다양한 상태를 수신할 수 있도록 합니다.
상태 확인 은 코드가 이러한 시나리오를 올바르게 처리하도록 합니다.
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};
const dependencyFunctionMock = mock.fn();
dependencyFunctionMock.mock.mockImplementation((arg) => {
return fakeDatabaseData;
});
// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
assert.strictEqual(name, `${fakeDatabaseData.first} ${fakeDatabaseData.last}`);
// ARRANGE
const dependencyFunctionMock = jest.fn();
const fakeDatabaseData = {first: 'John', last: 'Jones'};
dependencyFunctionMock.mockReturnValue(fakeDatabaseData);
// ACT
// date is returned by mock then transformed in SomeTestFunction()
const { name } = someTestFunction()
// ASSERT
expect(name).toBe(`${fakeDatabaseData.first} ${fakeDatabaseData.last}`);
import { it, expect, vi } from 'vitest';
// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};
const dependencyFunctionMock = vi.fn();
dependencyFunctionMock.mockReturnValue(fakeDatabaseData);
// ACT
// date is returned by mock then transformed in SomeTestFunction()
const { name } = someTestFunction()
// ASSERT
expect(name).toBe(`${fakeDatabaseData.first} ${fakeDatabaseData.last}`);
이전 테스트의 목적은 수행 someTestFunction
된 작업이 예상된 결과를 충족하는지 확인하는 것입니다. 이 간단한 예제에서 함수의 역할은 이름과 성을 연결하는 것입니다. 가짜 데이터를 사용하면 예상된 결과를 알고 함수가 작업을 올바르게 수행하는지 확인할 수 있습니다.
가짜
Fakes는 클라우드 데이터베이스 대신 메모리 내 데이터베이스를 사용하는 것과 같이 프로덕션 환경에서 일반적으로 사용하지 않는 기능을 대체합니다.
// fake-in-mem-db.spec.ts
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
class FakeDatabase {
private data: Record<string, any>;
constructor() {
this.data = {};
}
save(key: string, value: any): void {
this.data[key] = value;
}
get(key: string): any {
return this.data[key];
}
}
// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
db.save(key, value);
return db.get(key);
}
describe('In-Mem DB', () => {
let fakeDb: FakeDatabase;
let testKey: string;
let testValue: any;
beforeEach(() => {
fakeDb = new FakeDatabase();
testKey = 'testKey';
testValue = {
first: 'John',
last: 'Jones',
lastUpdated: new Date().toISOString(),
};
});
afterEach(() => {
// Restore all mocks created by node:test’s mock helper.
mock.restoreAll();
});
it('should save and return the correct value', () => {
// Create a spy on the save method using node:test's mock helper.
const saveSpy = mock.method(fakeDb, 'save').mock;
// Call the function under test.
const result = someTestFunction(fakeDb, testKey, testValue);
// Verify state.
assert.deepStrictEqual(result, testValue);
assert.strictEqual(result.first, 'John');
assert.strictEqual(result.last, 'Jones');
assert.strictEqual(result.lastUpdated, testValue.lastUpdated);
// Verify behavior
assert.strictEqual(saveSpy.callCount(), 1);
const calls = saveSpy.calls;
assert.deepStrictEqual(calls[0].arguments, [testKey, testValue]);
});
});
// fake-in-mem-db.spec.ts
class FakeDatabase {
private data: Record<string, any>;
constructor() {
this.data = {};
}
save(key: string, value: any): void {
this.data[key] = value;
}
get(key: string): any {
return this.data[key];
}
}
// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
db.save(key, value);
return db.get(key);
}
describe('someTestFunction', () => {
let fakeDb: FakeDatabase;
let testKey: string;
let testValue: any;
beforeEach(() => {
fakeDb = new FakeDatabase();
testKey = 'testKey';
testValue = {
first: 'John',
last: 'Jones',
lastUpdated: new Date().toISOString(),
};
});
afterEach(() => {
// Clear all mocks
jest.resetAllMocks();
});
test('should save and return the correct value', () => {
// Spy on the save method
jest.spyOn(fakeDb, 'save');
// Call the function under test.
const result = someTestFunction(fakeDb, testKey, testValue);
// Verify state
expect(result).toEqual(testValue);
expect(result.first).toBe('John');
expect(result.last).toBe('Jones');
expect(result.lastUpdated).toBe(testValue.lastUpdated);
// Verify behavior
expect(fakeDb.save).toHaveBeenCalledWith(testKey, testValue);
});
});
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
class FakeDatabase {
private data: Record<string, any>;
constructor() {
this.data = {};
}
save(key: string, value: any): void {
this.data[key] = value;
}
get(key: string): any {
return this.data[key];
}
}
// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
db.save(key, value);
return db.get(key);
}
describe('In-Mem DB', () => {
let fakeDb: FakeDatabase;
let testKey: string;
let testValue: any;
let saveSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
fakeDb = new FakeDatabase();
testKey = 'testKey';
testValue = {
first: 'John',
last: 'Jones',
lastUpdated: new Date().toISOString(),
};
// Create a spy on the save method.
saveSpy = vi.spyOn(fakeDb, 'save');
});
afterEach(() => {
// Restore the spied methods.
vi.restoreAllMocks();
});
it('should save and return the correct value', () => {
// Call the function under test.
const result = someTestFunction(fakeDb, testKey, testValue);
// Verify state.
expect(result).toEqual(testValue);
expect(result.first).toBe('John');
expect(result.last).toBe('Jones');
expect(result.lastUpdated).toBe(testValue.lastUpdated);
// Verify behavior using vi.spyOn.
expect(saveSpy).toHaveBeenCalledTimes(1);
expect(saveSpy).toHaveBeenCalledWith(testKey, testValue);
});
});
이전 테스트의 목적은 데이터베이스와 올바르게 상호 작용하는지 someTestFunction
확인하는 것입니다. 가짜 메모리 내 데이터베이스를 사용하면 실제 데이터베이스에 의존하지 않고 함수의 논리를 테스트하여 테스트를 더 빠르고 안정적으로 만들 수 있습니다.
시나리오: Azure SDK를 사용하여 Cosmos DB에 문서 삽입
모든 정보가 제출되고 확인 되면 Cosmos DB에 새 문서를 작성해야 하는 애플리케이션이 있다고 상상해 보십시오. 빈 양식이 제출되거나 정보가 예상 형식과 일치하지 않는 경우 애플리케이션은 데이터를 입력하지 않아야 합니다.
Cosmos DB는 예로 사용되지만 개념은 대부분의 JavaScript용 Azure SDK에 적용됩니다. 다음 함수는 이 기능을 캡처합니다.
// insertDocument.ts
import { Container } from '../data/connect-to-cosmos.js';
import type {
DbDocument,
DbError,
RawInput,
VerificationErrors,
} from '../data/model.js';
import Verify from '../data/verify.js';
export async function insertDocument(
container: Container,
doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
const isVerified: boolean = Verify.inputVerified(doc);
if (!isVerified) {
return { message: 'Verification failed' } as VerificationErrors;
}
try {
const { resource } = await container.items.create({
id: doc.id,
name: `${doc.first} ${doc.last}`,
});
return resource as DbDocument;
} catch (error: any) {
if (error instanceof Error) {
if ((error as any).code === 409) {
return {
message: 'Insertion failed: Duplicate entry',
code: 409,
} as DbError;
}
return { message: error.message, code: (error as any).code } as DbError;
} else {
return { message: 'An unknown error occurred', code: 500 } as DbError;
}
}
}
// insertDocument.ts
import { Container } from '../data/connect-to-cosmos';
import {
DbDocument,
DbError,
RawInput,
VerificationErrors,
} from '../data/model';
import { inputVerified } from '../data/verify';
export async function insertDocument(
container: Container,
doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
const isVerified: boolean = inputVerified(doc);
if (!isVerified) {
return { message: 'Verification failed' } as VerificationErrors;
}
try {
const { resource } = await container.items.create({
id: doc.id,
name: `${doc.first} ${doc.last}`,
});
return resource as DbDocument;
} catch (error: any) {
if (error instanceof Error) {
if ((error as any).code === 409) {
return {
message: 'Insertion failed: Duplicate entry',
code: 409,
} as DbError;
}
return { message: error.message, code: (error as any).code } as DbError;
} else {
return { message: 'An unknown error occurred', code: 500 } as DbError;
}
}
}
// insertDocument.ts
import { Container } from '../data/connect-to-cosmos.js';
import type {
DbDocument,
DbError,
RawInput,
VerificationErrors,
} from '../data/model.js';
import Verify from '../data/verify.js';
export async function insertDocument(
container: Container,
doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
const isVerified: boolean = Verify.inputVerified(doc);
if (!isVerified) {
return { message: 'Verification failed' } as VerificationErrors;
}
try {
const { resource } = await container.items.create({
id: doc.id,
name: `${doc.first} ${doc.last}`,
});
return resource as DbDocument;
} catch (error: any) {
if (error instanceof Error) {
if ((error as any).code === 409) {
return {
message: 'Insertion failed: Duplicate entry',
code: 409,
} as DbError;
}
return { message: error.message, code: (error as any).code } as DbError;
} else {
return { message: 'An unknown error occurred', code: 500 } as DbError;
}
}
}
비고
TypeScript 형식은 함수에서 사용하는 데이터의 종류를 정의하는 데 도움이 됩니다. Jest 또는 다른 JavaScript 테스트 프레임워크를 사용하기 위해 TypeScript가 필요하지는 않지만 형식이 안전한 JavaScript를 작성하는 데 필수적입니다.
이 애플리케이션의 함수는 다음과 같습니다.
기능 |
설명 |
insertDocument |
데이터베이스에 문서를 삽입합니다.
이것이 우리가 테스트하려는 것입니다. |
inputVerified |
스키마에 대한 입력 데이터를 확인합니다. 데이터가 올바른 형식인지 확인합니다(예: 유효한 전자 메일 주소, 올바른 형식의 URL). |
cosmos.items.create |
@azure/cosmos를 사용하는 Azure Cosmos DB에 대한 SDK 함수입니다.
이것이 우리가 조롱하고 싶은 것입니다. 패키지 소유자가 유지 관리하는 자체 테스트가 이미 있습니다. 들어오는 데이터가 확인을 통과한 경우 Cosmos DB 함수 호출이 수행되고 데이터가 반환되었는지 확인해야 합니다. |
테스트 프레임워크 종속성 설치
이 프레임워크는 Node.js LTS의 일부로 제공됩니다.
애플리케이션 디렉터리의 루트에서 다음 명령을 사용하여 Jest를 설치합니다.
npm install -D jest
애플리케이션 디렉터리의 루트에서 다음 명령을 사용하여 Vitest를 설치합니다.
npm install -D vitest
package.json
새 스크립트로 애플리케이션을 업데이트하여 소스 코드 파일을 테스트합니다. 소스 코드 파일은 부분 파일 이름 및 확장명에서 일치하여 정의됩니다. 테스트 실행기는 테스트 파일에 대한 일반적인 명명 규칙에 따라 파일을 <file-name>.spec.[jt]s
찾습니다. 이 패턴은 다음 예제와 같은 이름의 파일이 테스트 파일로 해석되고 테스트 실행기에서 실행됨을 의미합니다.
-
*
.test.js: 예를 들어 math.test.js
-
*
.spec.js: 예를 들어 math.spec.js
-
테스트 디렉터리에 있는 파일(예: tests/math.js
테스트 실행기를 사용하여 해당 테스트 파일 패턴을 지원하는 스크립트를 package.json 추가합니다.
"scripts": {
"test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}
package.json
새 스크립트로 애플리케이션을 업데이트하여 소스 코드 파일을 테스트합니다. 소스 코드 파일은 부분 파일 이름 및 확장명에서 일치하여 정의됩니다. Jest는 테스트 파일에 대한 일반적인 명명 규칙에 따라 파일을 <file-name>.spec.[jt]s
찾습니다. 이 패턴은 다음 예제와 같이 명명된 파일이 테스트 파일로 해석되고 Jest에서 실행됨을 의미합니다.
-
*
.test.js: 예를 들어 math.test.js
-
*
.spec.js: 예를 들어 math.spec.js
-
테스트 디렉터리에 있는 파일(예: tests/math.js
Jest 를 사용하여 해당 테스트 파일 패턴을 지원하도록 스크립트를package.json에 추가합니다.
"scripts": {
"test": "jest dist --coverage",
}
TypeScript 소스 코드는 Jest가 dist
파일을 찾아 실행할 수 있는 *.spec.js
하위 폴더에 생성됩니다.
package.json
새 스크립트로 애플리케이션을 업데이트하여 소스 코드 파일을 테스트합니다. 소스 코드 파일은 부분 파일 이름 및 확장명에서 일치하여 정의됩니다. Vitest는 테스트 파일에 대한 일반적인 명명 규칙에 따라 파일을 <file-name>.spec.[jt]s
찾습니다. 이 패턴은 다음 예제와 같이 명명된 파일이 테스트 파일로 해석되고 Vitest에서 실행됨을 의미합니다.
-
*
.test.js: 예를 들어 math.test.js
-
*
.spec.js: 예를 들어 math.spec.js
-
테스트 디렉터리에 있는 파일(예: tests/math.js
Vitest를 사용하여 해당 테스트 파일 패턴을 지원하는 스크립트를 package.json 추가합니다.
"scripts": {
"test": "vitest run --coverage",
}
Azure SDK에 대한 단위 테스트 설정
모의 함수, 스텁 및 가짜를 사용하여 insertDocument 함수를 테스트하려면 어떻게 해야 할까요?
- 모의: 함수 동작을 다음과 같이 테스트하려면 모의 함수가 필요합니다.
- 데이터가 확인을 통과하면 Cosmos DB 함수에 대한 호출이 단 한 번만 수행됩니다.
- 데이터가 확인을 통과하지 못하면 Cosmos DB 함수에 대한 호출이 수행되지 않았습니다.
- 스텁:
- 전달된 데이터는 함수에서 반환된 새 문서와 일치합니다.
테스트할 때 테스트 설정, 테스트 자체 및 확인의 관점에서 생각해 보세요. 테스트 언어 측면에서 이 기능은 다음 용어를 사용합니다.
- 정렬: 테스트 조건 설정
- 작업: 평가를 위해 당신의 함수를 호출합니다. 이는 테스트 중인 시스템(SUT)이라고도 합니다.
- 어설션: 결과의 유효성을 검사합니다. 결과는 동작 또는 상태일 수 있습니다.
- 동작은 확인할 수 있는 테스트 함수의 기능을 나타냅니다. 한 예로, 일부 종속성이 호출된 경우가 있습니다.
- 상태는 함수에서 반환된 데이터를 나타냅니다.
import { describe, it, afterEach, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
describe('boilerplate', () => {
beforeEach(() => {
// Setup required before each test
});
afterEach(() => {
// Cleanup required after each test
});
it('should <do something> if <situation is present>', async () => {
// Arrange
// - set up the test data and the expected result
// Act
// - call the function to test
// Assert
// - check the state: result returned from function
// - check the behavior: dependency function calls
});
});
Jest에는 테스트 파일을 정의하는 테스트 파일 템플릿이 있습니다.
// boilerplate.spec.ts
describe('nameOfGroupOfTests', () => {
beforeEach(() => {
// Setup required before each test
});
afterEach(() => {
// Cleanup required after each test
});
it('should <do something> if <situation is present>', async () => {
// Arrange
// - set up the test data and the expected result
// Act
// - call the function to test
// Assert
// - check the state: result returned from function
// - check the behavior: dependency function calls
});
});
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
describe('boilerplate', () => {
beforeEach(() => {
// Setup required before each test
});
afterEach(() => {
// Cleanup required after each test
});
it('should <do something> if <situation is present>', async () => {
// Arrange
// - set up the test data and the expected result
// Act
// - call the function to test
// Assert
// - check the state: result returned from function
// - check the behavior: dependency function calls
});
});
테스트에서 모의 개체를 사용하는 경우 해당 템플릿 코드는 Azure 클라이언트 라이브러리와 같이 함수에 사용되는 기본 종속성을 호출하지 않고 모의 메서드를 사용하여 함수를 테스트해야 합니다.
테스트 파일 만들기
모의 파일이 있는 테스트 파일은 종속성 호출을 시뮬레이션하기 위해 추가 설정이 있습니다.
테스트 파일에는 다음과 같은 몇 가지 부분이 있습니다.
-
import
: import 문을 사용하면 테스트를 실행하거나 모의 테스트를 작성할 수 있습니다.
-
mock
: 원하는 기본 모의 동작을 만듭니다. 각 테스트는 필요에 따라 변경될 수 있습니다.
-
describe
: insert.ts
파일의 테스트 그룹 가족.
-
it
: insert.ts
파일에 대한 각 테스트입니다.
테스트 파일에는 다음과 같은 몇 가지 부분이 있습니다.
-
import
: import 문을 사용하면 테스트를 실행하거나 모의 테스트를 작성할 수 있습니다.
-
jest.mock
: 원하는 기본 모의 동작을 만듭니다. 각 테스트는 필요에 따라 변경될 수 있습니다.
-
describe
: insert.ts
파일의 테스트 그룹 가족.
-
test
: insert.ts
파일에 대한 각 테스트입니다.
테스트 파일에는 다음과 같은 몇 가지 부분이 있습니다.
-
import
: import 문을 사용하면 테스트를 실행하거나 모의 테스트를 작성할 수 있습니다.
-
vi.spyOn
: 기본 스파이를 만든 다음 , .를 사용하여 모의 동작을 .mockReturnValue
추가합니다. 각 테스트는 필요에 따라 변경될 수 있습니다.
-
describe
: insert.ts
파일의 테스트 그룹 가족.
-
it
: insert.ts
파일에 대한 각 테스트입니다.
테스트 파일은 두 가지 유효성 검사 유형으로 나눌 수 있는 파일에 대한 insert.ts
세 가지 테스트를 다룹니다.
유효성 검사 유형 |
테스트 |
행복한 경로: should insert document successfully |
모의 데이터베이스 메서드가 호출되고 변경된 데이터가 반환되었습니다. |
오류 경로: should return verification error if input is not verified |
데이터가 유효성 검사에 실패하고 오류를 반환했습니다. |
오류 경로:should return error if db insert fails |
모의 데이터베이스 메서드가 호출되고 오류가 반환되었습니다. |
다음 테스트 파일은 insertDocument 함수를 테스트하는 방법을 보여줍니다.
// insertDocument.test.ts
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
import { Container } from '../src/data/connect-to-cosmos.js';
import { createTestInputAndResult } from '../src/data/fake-data.js';
import type { DbDocument, DbError, RawInput } from '../src/data/model.js';
import { isDbError, isVerificationErrors } from '../src/data/model.js';
import Verify from '../src/data/verify.js';
import CosmosConnector from '../src/data/connect-to-cosmos.js';
import { insertDocument } from '../src/lib/insert.js';
describe('SDK', () => {
beforeEach(() => {
// Clear all mocks before each test
mock.restoreAll();
});
it('should return verification error if input is not verified', async () => {
const fakeContainer = {
items: {
create: async (_: any) => {
throw new Error('Create method not implemented');
},
},
} as unknown as Container;
const mVerify = mock.method(Verify, 'inputVerified').mock;
mVerify.mockImplementation(() => false);
const mGetUniqueId = mock.method(CosmosConnector, 'getUniqueId').mock;
mGetUniqueId.mockImplementation(() => 'unique-id');
const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
// Arrange: wrong shape of document on purpose.
const doc = { name: 'test' } as unknown as RawInput;
// Act:
const insertDocumentResult = await insertDocument(fakeContainer, doc);
// Assert - State verification.
if (isVerificationErrors(insertDocumentResult)) {
assert.deepStrictEqual(insertDocumentResult, {
message: 'Verification failed',
});
} else {
throw new Error('Result is not of type VerificationErrors');
}
// Assert - Behavior verification: Verify that create was never called.
assert.strictEqual(mContainerCreate.callCount(), 0);
});
it('should insert document successfully', async () => {
// Arrange: override inputVerified to return true.
const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
createTestInputAndResult();
const fakeContainer = {
items: {
create: async (doc: any) => {
return { resource: result };
},
},
} as unknown as Container;
const mVerify = mock.method(Verify, 'inputVerified').mock;
mVerify.mockImplementation(() => true);
const mContainerCreate = mock.method(
fakeContainer.items as any,
'create',
).mock;
mContainerCreate.mockImplementation(async (doc: any) => {
return { resource: result };
});
// Act:
const receivedResult = await insertDocument(fakeContainer, input);
// Assert - State verification: Ensure the result is as expected.
assert.deepStrictEqual(receivedResult, result);
// Assert - Behavior verification: Ensure create was called once with correct arguments.
assert.strictEqual(mContainerCreate.callCount(), 1);
assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
id: input.id,
name: result.name,
});
});
it('should return error if db insert fails', async () => {
// Arrange: override inputVerified to return true.
const { input, result } = createTestInputAndResult();
const errorMessage: string = 'An unknown error occurred';
const fakeContainer = {
items: {
create: async (doc: any): Promise<any> => {
return Promise.resolve(null);
},
},
} as unknown as Container;
const mVerify = mock.method(Verify, 'inputVerified').mock;
mVerify.mockImplementation(() => true);
const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
mContainerCreate.mockImplementation(async (doc: any) => {
const mockError: DbError = {
message: errorMessage,
code: 500,
};
throw mockError;
});
// Act:
const insertDocumentResult = await insertDocument(fakeContainer, input);
// // Assert - Ensure create method was called once with the correct arguments.
assert.strictEqual(isDbError(insertDocumentResult), true);
assert.strictEqual(mContainerCreate.callCount(), 1);
assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
id: input.id,
name: result.name,
});
});
});
// insertDocument.test.ts
import { Container } from '../data/connect-to-cosmos';
import { createTestInputAndResult } from '../data/fake-data';
import type { DbDocument, DbError, RawInput } from '../data/model';
import { isDbError, isVerificationErrors } from '../data/model';
import { inputVerified } from '../data/verify';
import { insertDocument } from './insert';
// Mock app dependencies for Cosmos DB setup
jest.mock('../data/connect-to-cosmos', () => ({
connectToContainer: jest.fn(),
getUniqueId: jest.fn().mockReturnValue('unique-id'),
}));
// Mock app dependencies for input verification
jest.mock('../data/verify', () => ({
inputVerified: jest.fn(),
}));
describe('SDK', () => {
let mockContainer: jest.Mocked<Container>;
beforeEach(() => {
// Clear all mocks before each test
jest.resetAllMocks();
// Mock the Cosmos DB Container create method
mockContainer = {
items: {
create: jest.fn(),
},
} as unknown as jest.Mocked<Container>;
});
it('should return verification error if input is not verified', async () => {
// Arrange - Mock the input verification function to return false
jest.mocked(inputVerified).mockReturnValue(false);
// Arrange - wrong shape of doc on purpose
const doc = { name: 'test' };
// Act - Call the function to test
const insertDocumentResult = await insertDocument(
mockContainer,
doc as unknown as RawInput,
);
// Assert - State verification: Check the result when verification fails
if (isVerificationErrors(insertDocumentResult)) {
expect(insertDocumentResult).toEqual({
message: 'Verification failed',
});
} else {
throw new Error('Result is not of type VerificationErrors');
}
// Assert - Behavior verification: Ensure create method was not called
expect(mockContainer.items.create).not.toHaveBeenCalled();
});
it('should insert document successfully', async () => {
// Arrange - create input and expected result data
const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
createTestInputAndResult();
// Arrange - mock the input verification function to return true
(inputVerified as jest.Mock).mockReturnValue(true);
(mockContainer.items.create as jest.Mock).mockResolvedValue({
resource: result,
});
// Act - Call the function to test
const insertDocumentResult = await insertDocument(mockContainer, input);
// Assert - State verification: Check the result when insertion is successful
expect(insertDocumentResult).toEqual(result);
// Assert - Behavior verification: Ensure create method was called with correct arguments
expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
expect(mockContainer.items.create).toHaveBeenCalledWith({
id: input.id,
name: result.name,
});
});
it('should return error if db insert fails', async () => {
// Arrange - create input and expected result data
const { input, result } = createTestInputAndResult();
// Arrange - mock the input verification function to return true
jest.mocked(inputVerified).mockReturnValue(true);
// Arrange - mock the Cosmos DB create method to throw an error
const mockError: DbError = {
message: 'An unknown error occurred',
code: 500,
};
jest.mocked(mockContainer.items.create).mockRejectedValue(mockError);
// Act - Call the function to test
const insertDocumentResult = await insertDocument(mockContainer, input);
// Assert - verify type as DbError
if (isDbError(insertDocumentResult)) {
expect(insertDocumentResult.message).toBe(mockError.message);
} else {
throw new Error('Result is not of type DbError');
}
// Assert - Behavior verification: Ensure create method was called with correct arguments
expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
expect(mockContainer.items.create).toHaveBeenCalledWith({
id: input.id,
name: result.name,
});
});
});
// insertDocument.test.ts
import { describe, it, beforeEach, expect, vi } from 'vitest';
import type { Container, ItemResponse } from '@azure/cosmos';
import { insertDocument } from '../src/lib/insert.js';
import { createTestInputAndResult } from '../src/data/fake-data.js';
import type { DbDocument, DbError, RawInput } from '../src/data/model.js';
import { isDbError, isVerificationErrors } from '../src/data/model.js';
import Verify from '../src/data/verify.js';
describe('insertDocument', () => {
let fakeContainer: Container;
beforeEach(() => {
// Clear all mocks before each test
vi.restoreAllMocks();
// Create a fake container with a mocked `create` function
fakeContainer = {
items: {
create: vi.fn(),
},
} as unknown as Container;
});
it('should return verification error if input is not verified', async () => {
// Arrange – mock the input verification function to return false.
const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified');
inputVerifiedMock.mockReturnValue(false);
const doc = { name: 'test' };
// Act – call the function under test.
const insertDocumentResult = await insertDocument(
fakeContainer,
doc as unknown as RawInput,
);
// Assert – state verification: result should indicate verification failure.
if (isVerificationErrors(insertDocumentResult)) {
expect(insertDocumentResult).toEqual({
message: 'Verification failed',
} as unknown as DbError);
} else {
throw new Error('Result is not of type VerificationErrors');
}
// Assert – behavior verification: ensure create method was not called.
expect(fakeContainer.items.create).not.toHaveBeenCalled();
expect(inputVerifiedMock).toHaveBeenCalledTimes(1);
});
it('should insert document successfully', async () => {
// Prepare test data
const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
createTestInputAndResult();
const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified');
inputVerifiedMock.mockReturnValue(true);
// Set up the mocked return value.
// Here we "cast" our minimal object to satisfy the expected type ItemResponse<DbDocument>.
(
fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue({
resource: result,
// Minimal additional properties required by ItemResponse.
item: result,
headers: {},
statusCode: 201,
diagnostics: {} as any,
requestCharge: 0,
activityId: 'fake-activity-id',
} as unknown as ItemResponse<DbDocument>);
// Call the function under test that internally calls container.items.create.
const insertDocumentResult = await insertDocument(fakeContainer, input);
// Validate the returned value.
expect(insertDocumentResult).toEqual(result);
// Validate that create was called once with the proper arguments.
expect(inputVerifiedMock).toHaveBeenCalledTimes(1);
expect(fakeContainer.items.create).toHaveBeenCalledTimes(1);
expect(
(fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>).mock
.calls[0][0],
).toEqual({
id: input.id,
name: result.name,
});
});
it('should return error if db insert fails', async () => {
// Arrange – create input and expected result data.
const { input, result } = createTestInputAndResult();
// Arrange – mock the input verification to return true.
const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified');
inputVerifiedMock.mockReturnValue(true);
// Arrange – mock the create method to reject with an error.
const mockError: DbError = {
message: 'An unknown error occurred',
code: 500,
};
(
fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>
).mockRejectedValue(mockError as unknown as DbError);
// Act – call the function under test.
const insertDocumentResult = await insertDocument(fakeContainer, input);
// Assert – verify result is of type DbError.
if (isDbError(insertDocumentResult)) {
expect(insertDocumentResult.message).toBe(mockError.message);
} else {
throw new Error('Result is not of type DbError');
}
// Assert – behavior verification: ensure create was called once with correct arguments.
expect(inputVerifiedMock).toHaveBeenCalledTimes(1);
expect(fakeContainer.items.create).toHaveBeenCalledTimes(1);
expect(
(fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>).mock
.calls[0][0],
).toEqual({
id: input.id,
name: result.name,
});
});
});
문제 해결
이 문서의 코드 대부분은 MicrosoftDocs/node-essentials GitHub 리포지토리에서 제공됩니다. Cosmos DB 리소스에 데이터를 삽입하려면 이 스크립트를 사용하여 리소스를 생성하십시오.