共用方式為


在 JavaScript 應用程式中測試 Azure SDK 整合

針對適用於 JavaScript 的 Azure SDK 測試整合程式碼,是確保應用程式與 Azure 服務正確互動的必要條件。

在決定是否模擬雲端服務 SDK 呼叫或使用即時服務進行測試時,請務必考慮速度、可靠性和成本之間的取捨。

必要條件

模擬雲端服務

優點

  • 藉由消除網路等待時間來加速測試套件。
  • 提供可預測且受控制的測試環境。
  • 更容易模擬各種案例和邊緣案例。
  • 降低與使用即時雲端服務相關的成本,特別是在持續整合管線中。

缺點

  • 模擬可能會偏離實際 SDK,導致差異。
  • 可能會忽略即時服務的特定功能或行為。
  • 相較於生產環境,環境比較不現實。

使用即時服務

優點

  • 提供與生產環境密切鏡像的實際環境。
  • 適合用於整合測試,以確保系統的不同部分一起運作。
  • 協助識別與網路可靠性、服務可用性和實際數據處理相關的問題。

缺點

  • 由於網路呼叫而變慢。
  • 由於潛在的服務使用量成本,因此成本更高。
  • 設定和維護符合生產環境的即時服務環境,非常複雜且耗時。

模擬和使用即時服務之間的選擇取決於您的測試策略。 對於速度和控制至關重要的單元測試,模擬通常是更好的選擇。 對於現實主義至關重要的整合測試,使用即時服務可以提供更精確的結果。 平衡這些方法有助於達到完整的測試涵蓋範圍,同時管理成本和維護測試效率。

測試雙打:模擬、存根和假貨

測試雙精度浮點數是用來取代實際用於測試目的的任何替代專案。 您選擇的雙精度浮點數類型是根據您想要取代的。 當字詞隨意使用時,模擬詞通常表示為任何雙精度浮點數。 在本文中,該詞彙會特別使用,並特別說明在 Jest 測試架構中。

模擬

模擬(也稱為 間諜):在函式中替代 ,並能夠在其他程式碼間接呼叫該函式時控制及監視 該函式的行為

在下列範例中,您有 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 為此 提供部分模擬
  • 較小相依性的所有函式,如本文範例所示。

Stub

存根的目的是要取代函式的傳回數據,以模擬不同的案例。 這可讓您的程式代碼呼叫函式並接收各種狀態,包括成功的結果、失敗、例外狀況和邊緣案例。 狀態驗證 可確保您的程式代碼正確處理這些案例。

// 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 類型有助於定義函式所使用的數據類型。 雖然您不需要 TypeScript 使用 Jest 或其他 JavaScript 測試架構,但撰寫類型安全 JavaScript 是不可或缺的。

上述應用程式內的函式如下:

函式 描述
insertDocument 將檔案插入資料庫中。 這就是我們想要測試的內容。
inputVerified 根據架構驗證輸入數據。 確保數據的格式正確(例如,有效的電子郵件位址、格式正確的URL)。
cosmos.items.create 使用 @azure/cosmos DB 的 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
  • 位於測試目錄中的檔案,例如test/math.js

將腳本新增至 package.json ,以支援 Jest 的測試檔案模式:

"scripts": {
    "test": "jest dist",
}

TypeScript 原始程式碼會產生到 dist 子資料夾中。 Jest 會 .spec.js 執行子資料夾中找到的 dist 檔案。

設定 Azure SDK 的單元測試

如何使用模擬、存根和假貨來測試 insertDocument 函式?

  • 模擬:我們需要模擬,以確保 函式的行為 已經過測試,例如:
    • 如果數據確實通過驗證,則對 Cosmos DB 函式的呼叫只會發生 1 次
    • 如果數據未通過驗證,則不會呼叫 Cosmos DB 函式
  • 存根:
    • 傳入的數據符合函式傳回的新檔。

測試時,請考慮測試設定、測試本身和驗證。 在測試白話方面,這稱為:

  • 排列:設定測試條件
  • Act:呼叫您的函式以測試,也稱為 受測 系統或 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 三個測試,可分成兩種驗證類型:

驗證類型 Test
快樂路徑: 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,
    });
  });
});

其他資訊