다음을 통해 공유


JavaScript 애플리케이션에서 Azure SDK 통합 테스트

애플리케이션이 Azure 서비스와 올바르게 상호 작용하도록 하려면 JavaScript용 Azure SDK에 대한 통합 코드를 테스트해야 합니다.

클라우드 서비스 SDK 호출을 모의할지 아니면 테스트 목적으로 라이브 서비스를 사용할지 결정할 때 속도, 안정성 및 비용 간의 장단점이 고려되어야 합니다.

필수 조건

클라우드 서비스 모의

장점:

  • 네트워크 대기 시간을 제거하여 테스트 제품군의 속도를 향상합니다.
  • 예측 가능하고 제어된 테스트 환경을 제공합니다.
  • 다양한 시나리오 및 에지 사례를 보다 쉽게 시뮬레이션할 수 있습니다.
  • 특히 연속 통합 파이프라인에서 라이브 클라우드 서비스 사용과 관련된 비용을 줄입니다.

단점:

  • 모의 개체는 실제 SDK에서 표류하여 불일치로 이어질 수 있습니다.
  • 라이브 서비스의 특정 기능 또는 동작을 무시할 수 있습니다.
  • 프로덕션에 비해 덜 현실적인 환경입니다.

라이브 서비스 사용

장점:

  • 프로덕션을 밀접하게 반영하는 현실적인 환경을 제공합니다.
  • 시스템의 여러 부분이 함께 작동하도록 통합 테스트에 유용합니다.
  • 네트워크 안정성, 서비스 가용성 및 실제 데이터 처리와 관련된 문제를 식별하는 데 도움이 됩니다.

단점:

  • 네트워크 호출로 인해 속도가 느려집니다.
  • 잠재적인 서비스 사용 비용으로 인해 비용이 더 많이 듭니다.
  • 프로덕션과 일치하는 라이브 서비스 환경을 설정하고 유지 관리하는 데 복잡하고 시간이 많이 걸립니다.

모의 및 라이브 서비스 사용 중에서 선택하는 것은 테스트 전략에 따라 달라집니다. 속도와 제어가 가장 중요한 단위 테스트의 경우 모의 작업이 더 나은 선택인 경우가 많습니다. 리얼리즘이 중요한 통합 테스트의 경우 라이브 서비스를 사용하면 보다 정확한 결과를 제공할 수 있습니다. 이러한 접근 방식의 균형을 맞추면 비용을 관리하고 테스트 효율성을 유지하면서 포괄적인 테스트 범위를 달성할 수 있습니다.

테스트 더블: 모의, 스텁 및 가짜

테스트 더블은 테스트 목적으로 실제 항목 대신 사용되는 모든 종류의 대체품입니다. 선택한 double 형식은 바꿀 형식을 기반으로 합니다. 모의 용어는 용어가 아무렇지 않게 사용될 때 종종 모든 이중으로 의미됩니다. 이 문서에서는 이 용어가 Jest 테스트 프레임워크에서 구체적으로 사용되고 구체적으로 설명되어 있습니다.

Mock(모의)

모의(스파이라고도 함): 함수를 대체하고 다른 코드에서 간접적으로 호출될 때 해당 함수의 동작을 제어하고 감시할 수 있습니다.

다음 예제에는 2개의 함수가 있습니다.

  • someTestFunction: 테스트해야 하는 함수입니다. 종속성을 호출합니다. 이 종속성은 dependencyFunction작성하지 않았으며 테스트할 필요가 없습니다.
  • dependencyFunctionMock: 종속성의 모의 항목입니다.
// setup
const dependencyFunctionMock = jest.fn();

// perform test
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// verify behavior
expect(dependencyFunctionMock).toHaveBeenCalled();

테스트의 목적은 실제로 종속성 코드를 호출하지 않고 someTestFunction이 올바르게 작동하는지 확인하는 것입니다. 이 테스트는 종속성의 모의 개체가 호출되었는지 확인합니다.

모의 대용량 종속성 및 작은 종속성

종속성을 모의하기로 결정한 경우 다음과 같이 필요한 항목만 모의하도록 선택할 수 있습니다.

  • 더 큰 종속성의 함수 또는 두 개입니다. Jest는 이 목적을 위해 부분적인 모의를 제공합니다.
  • 이 문서의 예제와 같이 더 작은 종속성의 모든 함수입니다.

스텁

스텁의 목적은 함수의 반환 데이터를 대체하여 다양한 시나리오를 시뮬레이션하는 것입니다. 이렇게 하면 코드에서 함수를 호출하고 성공적인 결과, 실패, 예외 및 에지 사례를 비롯한 다양한 상태를 수신할 수 있습니다. 상태 확인 은 코드가 이러한 시나리오를 올바르게 처리하도록 합니다.

// 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(`${first} ${last}`);

이전 테스트의 목적은 수행 someTestFunction 된 작업이 예상된 결과를 충족하는지 확인하는 것입니다. 이 간단한 예제에서 함수의 작업은 이름과 패밀리 이름을 연결합니다. 가짜 데이터를 사용하면 예상된 결과를 알고 함수가 작업을 올바르게 수행하는지 확인할 수 있습니다.

Fakes

Fakes는 클라우드 데이터베이스 대신 메모리 내 데이터베이스를 사용하는 것과 같이 프로덕션 환경에서 일반적으로 사용하지 않는 기능을 대체합니다.

// 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);
}

// Jest test suite
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(),
    };

    // Spy on the save method
    jest.spyOn(fakeDb, 'save');
  });

  afterEach(() => {
    // Clear all mocks
    jest.clearAllMocks();
  });

  test('should save and return the correct value', () => {
    // Perform 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);
  });
});

이전 테스트의 목적은 데이터베이스와 올바르게 상호 작용하는지 someTestFunction 확인하는 것입니다. 가짜 메모리 내 데이터베이스를 사용하면 실제 데이터베이스에 의존하지 않고 함수의 논리를 테스트하여 테스트를 더 빠르고 안정적으로 만들 수 있습니다.

시나리오: Azure SDK를 사용하여 Cosmos DB에 문서 삽입

모든 정보가 제출되고 확인되면 Cosmos DB에 새 문서를 작성해야 하는 애플리케이션이 있다고 상상해 보십시오. 빈 양식이 제출되거나 정보가 예상 형식과 일치하지 않는 경우 애플리케이션은 데이터를 입력하지 않아야 합니다.

Cosmos DB는 예로 사용되지만 개념은 대부분의 JavaScript용 Azure SDK에 적용됩니다. 다음 함수는 이 기능을 캡처합니다.

// 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;
    }
  }
}

참고 항목

TypeScript 형식은 함수에서 사용하는 데이터의 종류를 정의하는 데 도움이 됩니다. Jest 또는 다른 JavaScript 테스트 프레임워크를 사용하기 위해 TypeScript가 필요하지는 않지만 형식이 안전한 JavaScript를 작성하는 데 필수적입니다.

위의 이 애플리케이션의 함수는 다음과 같습니다.

함수 설명
insertDocument 데이터베이스에 문서를 삽입합니다. 이것이 우리가 테스트하려는 것입니다.
inputVerified 스키마에 대한 입력 데이터를 확인합니다. 데이터가 올바른 형식인지 확인합니다(예: 유효한 전자 메일 주소, 올바른 형식의 URL).
cosmos.items.create @azure/cosmos를 사용하는 Azure Cosmos DB에 대한 SDK 함수입니다. 이것이 우리가 조롱하고 싶은 것입니다. 패키지 소유자가 유지 관리하는 자체 테스트가 이미 있습니다. 들어오는 데이터가 확인을 통과한 경우 Cosmos DB 함수 호출이 수행되고 데이터가 반환되었는지 확인해야 합니다.

테스트 프레임워크 종속성 설치

이 문서에서는 Jest를 테스트 프레임워크로 사용합니다. 사용할 수 있는 다른 테스트 프레임워크도 있습니다.

애플리케이션 디렉터리의 루트에서 다음 명령을 사용하여 Jest를 설치합니다.

npm install jest

테스트를 실행하도록 패키지 구성

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",
}

TypeScript 소스 코드는 하위 폴더에 dist 생성됩니다. Jest는 .spec.js 하위 폴더에 있는 dist 파일을 실행합니다.

Azure SDK에 대한 단위 테스트 설정

모의 함수, 스텁 및 가짜를 사용하여 insertDocument 함수를 테스트하려면 어떻게 해야 할까요?

  • 모의: 함수 동작을 다음과 같이 테스트하려면 모의 함수가 필요합니다.
    • 데이터가 확인을 통과하면 Cosmos DB 함수에 대한 호출이 단 한 번만 수행됩니다.
    • 데이터가 확인을 통과하지 못하면 Cosmos DB 함수에 대한 호출이 수행되지 않았습니다.
  • 스텁을:
    • 전달된 데이터는 함수에서 반환된 새 문서와 일치합니다.

테스트할 때 테스트 설정, 테스트 자체 및 확인의 관점에서 생각해 보세요. 테스트 언어를 기준으로 다음과 같이 알려져 있습니다.

  • 정렬: 테스트 조건 설정
  • 작업: 테스트 중인 시스템 또는 SUT라고도 하는 테스트를 위해 함수를 호출합니다.
  • 어설션: 결과의 유효성을 검사합니다. 결과는 동작 또는 상태일 수 있습니다.
    • 동작은 확인할 수 있는 테스트 함수의 기능을 나타냅니다. 한 가지 예는 일부 종속성이 호출되었다는 것입니다.
    • 상태는 함수에서 반환된 데이터를 나타냅니다.

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
  });
});

모의 항목을 사용하는 경우 해당 보일러 장소는 Azure 클라이언트 라이브러리와 같이 함수에 사용되는 기본 종속성을 호출하지 않고 모의 작업을 사용하여 함수를 테스트해야 합니다.

테스트 파일 만들기

종속성 호출을 시뮬레이트하기 위한 모의 테스트 파일에는 일반적인 테스트 상용구 코드 외에 몇 가지 추가 설정이 있습니다. 테스트 파일에는 다음과 같은 몇 가지 부분이 있습니다.

  • import: import 문을 사용하면 테스트를 사용하거나 모의할 수 있습니다.
  • jest.mock: 원하는 기본 모의 동작을 만듭니다. 각 테스트는 필요에 따라 변경될 수 있습니다.
  • describe: 파일에 대한 그룹 패밀리를 테스트합니다 insert.ts .
  • test: 파일에 대한 각 테스트입니다 insert.ts .

테스트 파일은 두 가지 유효성 검사 유형으로 나눌 수 있는 파일에 대한 insert.ts 세 가지 테스트를 다룹니다.

유효성 검사 유형 테스트
행복한 경로: should insert document successfully 모의 데이터베이스 메서드가 호출되고 변경된 데이터가 반환되었습니다.
오류 경로: should return verification error if input is not verified 데이터가 유효성 검사에 실패하고 오류를 반환했습니다.
오류 경로:should return error if db insert fails 모의 데이터베이스 메서드가 호출되고 오류가 반환되었습니다.

다음 Jest 테스트 파일은 insertDocument 함수를 테스트하는 방법을 보여줍니다.

// insertDocument.test.ts
import { Container } from '../data/connect-to-cosmos';
import { createTestInputAndResult } from '../data/fake-data';
import {
  DbDocument,
  DbError,
  isDbError,
  isVerificationErrors,
  RawInput,
} 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('insertDocument', () => {
  // Mock the Cosmo DB Container object
  let mockContainer: jest.Mocked<Container>;

  beforeEach(() => {
    // Clear all mocks before each test
    jest.clearAllMocks();

    // 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,
    });
  });
});

추가 정보