自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何為 Nest.js 編寫單元測試和 E2E 測試

開發(fā) 前端
單元測試是對軟件中的最小可測試單元進(jìn)行檢查和驗(yàn)證。比如一個(gè)函數(shù)、一個(gè)方法都可以是一個(gè)單元。在單元測試中,你會(huì)對這個(gè)函數(shù)的各種輸入給出預(yù)期的輸出,并驗(yàn)證功能的正確性。單元測試的目標(biāo)是快速發(fā)現(xiàn)函數(shù)內(nèi)部的 bug,并且它們?nèi)菀拙帉?、快速?zhí)行。

前言

最近在給一個(gè) nestjs 項(xiàng)目寫單元測試(Unit Testing)和 e2e 測試(End-to-End Testing,端到端測試,簡稱 e2e 測試),這是我第一次給后端項(xiàng)目寫測試,發(fā)現(xiàn)和之前給前端項(xiàng)目寫測試還不太一樣,導(dǎo)致在一開始寫測試時(shí)感覺無從下手。后來在看了一些示例之后才想明白怎么寫測試,所以打算寫篇文章記錄并分享一下,以幫助和我有相同困惑的人。

同時(shí)我也寫了一個(gè) demo 項(xiàng)目,相關(guān)的單元測試、e2e 測試都寫好了,有興趣可以看一下。代碼已上傳到 Github: nestjs-interview-demo[1]。

單元測試和 E2E 測試的區(qū)別

單元測試和 e2e 測試都是軟件測試的方法,但它們的目標(biāo)和范圍有所不同。

單元測試是對軟件中的最小可測試單元進(jìn)行檢查和驗(yàn)證。比如一個(gè)函數(shù)、一個(gè)方法都可以是一個(gè)單元。在單元測試中,你會(huì)對這個(gè)函數(shù)的各種輸入給出預(yù)期的輸出,并驗(yàn)證功能的正確性。單元測試的目標(biāo)是快速發(fā)現(xiàn)函數(shù)內(nèi)部的 bug,并且它們?nèi)菀拙帉?、快速?zhí)行。

而 e2e 測試通常通過模擬真實(shí)用戶場景的方法來測試整個(gè)應(yīng)用,例如前端通常使用瀏覽器或無頭瀏覽器來進(jìn)行測試,后端則是通過模擬對 API 的調(diào)用來進(jìn)行測試。

在 nestjs 項(xiàng)目中,單元測試可能會(huì)測試某個(gè)服務(wù)(service)、某個(gè)控制器(controller)的一個(gè)方法,例如測試 Users 模塊中的 update 方法是否能正確的更新一個(gè)用戶。而一個(gè) e2e 測試可能會(huì)測試一個(gè)完整的用戶流程,如創(chuàng)建一個(gè)新用戶,然后更新他們的密碼,然后刪除該用戶。這涉及了多個(gè)服務(wù)和控制器。

編寫單元測試

為一個(gè)工具函數(shù)或者不涉及接口的方法編寫單元測試,是非常簡單的,你只需要考慮各種輸入并編寫相應(yīng)的測試代碼就可以了。但是一旦涉及到接口,那情況就復(fù)雜了。用代碼來舉例:

async validateUser(
  username: string,
  password: string,
): Promise<UserAccountDto> {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }


  if (entity.lockUntil && entity.lockUntil > Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds > 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }


    throw new UnauthorizedException(message);
  }


  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };


    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 >= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }


    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }


  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts > 0 ||
    (entity.lockUntil && entity.lockUntil > Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }


  return { userId: entity._id, username } as UserAccountDto;
}

上面的代碼是 auth.service.ts 文件里的一個(gè)方法 validateUser,主要用于驗(yàn)證登錄時(shí)用戶輸入的賬號(hào)密碼是否正確。它包含的邏輯如下:

1.根據(jù) username 查看用戶是否存在,如果不存在則拋出 401 異常(也可以是 404 異常)2.查看用戶是否被鎖定,如果被鎖定則拋出 401 異常和相關(guān)的提示文字3.將 password 加密后和數(shù)據(jù)庫中的密碼進(jìn)行對比,如果錯(cuò)誤則拋出 401 異常(連續(xù)三次登錄失敗會(huì)被鎖定賬戶 5 分鐘)4.如果登錄成功,則將之前登錄失敗的計(jì)數(shù)記錄進(jìn)行清空(如果有)并返回用戶 id 和 username 到下一階段

可以看到 validateUser 方法包含了 4 個(gè)處理邏輯,我們需要對這 4 點(diǎn)都編寫對應(yīng)的單元測試代碼,以確定整個(gè) validateUser 方法功能是正常的。

第一個(gè)測試用例

在開始編寫單元測試時(shí),我們會(huì)遇到一個(gè)問題,findOne 方法需要和數(shù)據(jù)庫進(jìn)行交互,它要通過 username 查找數(shù)據(jù)庫中是否存在對應(yīng)的用戶。但如果每一個(gè)單元測試都得和數(shù)據(jù)庫進(jìn)行交互,那測試起來會(huì)非常麻煩。所以可以通過 mock 假數(shù)據(jù)來實(shí)現(xiàn)這一點(diǎn)。

舉例,假如我們已經(jīng)注冊了一個(gè) woai3c 的用戶,那么當(dāng)用戶登錄時(shí),在 validateUser 方法中能夠通過 const entity = await this.usersService.findOne({ username }); 拿到用戶數(shù)據(jù)。所以只要確保這行代碼能夠返回想要的數(shù)據(jù),即使不和數(shù)據(jù)庫交互也是沒有問題的。而這一點(diǎn),我們能通過 mock 數(shù)據(jù)來實(shí)現(xiàn)?,F(xiàn)在來看一下 validateUser 方法的相關(guān)測試代碼:

import { Test } from '@nestjs/testing';
import { AuthService } from '@/modules/auth/auth.service';
import { UsersService } from '@/modules/users/users.service';
import { UnauthorizedException } from '@nestjs/common';
import { TEST_USER_NAME, TEST_USER_PASSWORD } from '@tests/constants';


describe('AuthService', () => {
  let authService: AuthService; // Use the actual AuthService type
  let usersService: Partial<Record<keyof UsersService, jest.Mock>>;


  beforeEach(async () => {
    usersService = {
      findOne: jest.fn(),
    };


    const module = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UsersService,
          useValue: usersService,
        },
      ],
    }).compile();


    authService = module.get<AuthService>(AuthService);
  });


  describe('validateUser', () => {
    it('should throw an UnauthorizedException if user is not found', async () => {
      await expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });


    // other tests...
  });
});

我們通過調(diào)用 usersService 的 fineOne 方法來拿到用戶數(shù)據(jù),所以需要在測試代碼中 mock usersService 的 fineOne 方法:

beforeEach(async () => {
    usersService = {
      findOne: jest.fn(), // 在這里 mock findOne 方法
    };


    const module = await Test.createTestingModule({
      providers: [
        AuthService, // 真實(shí)的 AuthService,因?yàn)槲覀円獙λ姆椒ㄟM(jìn)行測試
        {
          provide: UsersService, // 用 mock 的 usersService 代替真實(shí)的 usersService 
          useValue: usersService,
        },
      ],
    }).compile();


    authService = module.get<AuthService>(AuthService);
  });

通過使用 jest.fn() 返回一個(gè)函數(shù)來代替真實(shí)的 usersService.findOne()。如果這時(shí)調(diào)用 usersService.findOne() 將不會(huì)有任何返回值,所以第一個(gè)單元測試用例就能通過了:

it('should throw an UnauthorizedException if user is not found', async () => {
  await expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});

因?yàn)樵?nbsp;validateUser 方法中調(diào)用 const entity = await this.usersService.findOne({ username }); 的 findOne 是 mock 的假函數(shù),沒有返回值,所以 validateUser 方法中的第 2-4 行代碼就能執(zhí)行到了:

if (!entity) {
  throw new UnauthorizedException('User not found');
}

拋出 401 錯(cuò)誤,符合預(yù)期。

第二個(gè)測試用例

validateUser 方法中的第二個(gè)處理邏輯是判斷用戶是否鎖定,對應(yīng)的代碼如下:

if (entity.lockUntil && entity.lockUntil > Date.now()) {
  const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
  let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
  if (diffInSeconds > 60) {
    const diffInMinutes = Math.round(diffInSeconds / 60);
    message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
  }


  throw new UnauthorizedException(message);
}

可以看到如果用戶數(shù)據(jù)里有鎖定時(shí)間 lockUntil 并且鎖定結(jié)束時(shí)間大于當(dāng)前時(shí)間就可以判斷當(dāng)前賬戶處于鎖定狀態(tài)。所以需要 mock 一個(gè)具有 lockUntil 字段的用戶數(shù)據(jù):

it('should throw an UnauthorizedException if the account is locked', async () => {
  const lockedUser = {
    _id: TEST_USER_ID,
    username: TEST_USER_NAME,
    password: TEST_USER_PASSWORD,
    lockUntil: Date.now() + 1000 * 60 * 5, // The account is locked for 5 minutes
  };


  usersService.findOne.mockResolvedValueOnce(lockedUser);


  await expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});

在上面的測試代碼里,先定義了一個(gè)對象 lockedUser,這個(gè)對象里有我們想要的 lockUntil 字段,然后將它作為 findOne 的返回值,這通過 usersService.findOne.mockResolvedValueOnce(lockedUser); 實(shí)現(xiàn)。然后 validateUser 方法執(zhí)行時(shí),里面的用戶數(shù)據(jù)就是 mock 出來的數(shù)據(jù)了,從而成功讓第二個(gè)測試用例通過。

單元測試覆蓋率

剩下的兩個(gè)測試用例就不寫了,原理都是一樣的。如果剩下的兩個(gè)測試不寫,那么這個(gè) validateUser 方法的單元測試覆蓋率會(huì)是 50%,如果 4 個(gè)測試用例都寫完了,那么 validateUser 方法的單元測試覆蓋率將達(dá)到 100%。

單元測試覆蓋率(Code Coverage)是一個(gè)度量,用于描述應(yīng)用程序代碼有多少被單元測試覆蓋或測試過。它通常表示為百分比,表示在所有可能的代碼路徑中,有多少被測試用例覆蓋。

單元測試覆蓋率通常包括以下幾種類型:

?行覆蓋率(Lines):測試覆蓋了多少代碼行。?函數(shù)覆蓋率(Funcs):測試覆蓋了多少函數(shù)或方法。?分支覆蓋率(Branch):測試覆蓋了多少代碼分支(例如,if/else 語句)。?語句覆蓋率(Stmts):測試覆蓋了多少代碼語句。

單元測試覆蓋率是衡量單元測試質(zhì)量的一個(gè)重要指標(biāo),但并不是唯一的指標(biāo)。高的覆蓋率可以幫助檢測代碼中的錯(cuò)誤,但并不能保證代碼的質(zhì)量。覆蓋率低可能意味著有未被測試的代碼,可能存在未被發(fā)現(xiàn)的錯(cuò)誤。

下圖是 demo 項(xiàng)目的單元測試覆蓋率結(jié)果:

圖片圖片

像 service 和 controller 之類的文件,單元測試覆蓋率一般盡量高點(diǎn)比較好,而像 module 這種文件就沒有必要寫單元測試了,也沒法寫,沒有意義。上面的圖片表示的是整個(gè)單元測試覆蓋率的總體指標(biāo),如果你想查看某個(gè)函數(shù)的測試覆蓋率,可以打開項(xiàng)目根目錄下的 coverage/lcov-report/index.html 文件進(jìn)行查看。例如我想查看 validateUser 方法具體的測試情況:

圖片圖片

可以看到原來 validateUser 方法的單元測試覆蓋率并不是 100%,還是有兩行代碼沒有執(zhí)行到,不過也無所謂了,不影響 4 個(gè)關(guān)鍵的處理節(jié)點(diǎn),不要片面的追求高測試覆蓋率。

編寫E2E 測試

在單元測試中我們展示了如何為 validateUser() 的每一個(gè)功能點(diǎn)編寫單元測試,并且使用了 mock 數(shù)據(jù)的方法來確保每個(gè)功能點(diǎn)都能夠被測試到。而在 e2e 測試中,我們需要模擬真實(shí)的用戶場景,所以要連接數(shù)據(jù)庫來進(jìn)行測試。因此,這次測試的 auth.service.ts 模塊里的方法都會(huì)和數(shù)據(jù)庫進(jìn)行交互。

auth 模塊主要有以下幾個(gè)功能:

?注冊?登錄?刷新 token?讀取用戶信息?修改密碼?刪除用戶。

e2e 測試需要將這六個(gè)功能都測試一遍,從注冊開始,到刪除用戶結(jié)束。在測試時(shí),我們可以建一個(gè)專門的測試用戶來進(jìn)行測試,測試完成后再刪除這個(gè)測試用戶,這樣就不會(huì)在測試數(shù)據(jù)庫中留下無用的信息了。

beforeAll(async () => {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile()


  app = moduleFixture.createNestApplication()
  await app.init()


  // 執(zhí)行登錄以獲取令牌
  const response = await request(app.getHttpServer())
    .post('/auth/register')
    .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
    .expect(201)


  accessToken = response.body.access_token
  refreshToken = response.body.refresh_token
})


afterAll(async () => {
  await request(app.getHttpServer())
    .delete('/auth/delete-user')
    .set('Authorization', `Bearer ${accessToken}`)
    .expect(200)


  await app.close()
})

beforeAll 鉤子函數(shù)將在所有測試開始之前執(zhí)行,所以我們可以在這里注冊一個(gè)測試賬號(hào) TEST_USER_NAME。afterAll 鉤子函數(shù)將在所有測試結(jié)束之后執(zhí)行,所以在這刪除測試賬號(hào) TEST_USER_NAME 是比較合適的,還能順便對注冊和刪除兩個(gè)功能進(jìn)行測試。

在上一節(jié)的單元測試中,我們編寫了關(guān)于 validateUser 方法的相關(guān)單元測試。其實(shí)這個(gè)方法是在登錄時(shí)執(zhí)行的,用于驗(yàn)證用戶賬號(hào)密碼是否正確。所以這一次的 e2e 測試也將使用登錄流程來展示如何編寫 e2e 測試用例。

整個(gè)登錄測試流程總共包含了五個(gè)小測試:

describe('login', () => {
    it('/auth/login (POST)', () => {
      // ...
    })


    it('/auth/login (POST) with user not found', () => {
      // ...
    })


    it('/auth/login (POST) without username or password', async () => {
      // ...
    })


    it('/auth/login (POST) with invalid password', () => {
      // ...
    })


    it('/auth/login (POST) account lock after multiple failed attempts', async () => {
      // ...
    })
  })

這五個(gè)測試分別是:

1.登錄成功,返回 2002.如果用戶不存在,拋出 401 異常3.如果不提供密碼或用戶名,拋出 400 異常4.使用錯(cuò)誤密碼登錄,拋出 401 異常5.如果賬戶被鎖定,拋出 401 異常。

現(xiàn)在我們開始編寫 e2e 測試:

// 登錄成功
it('/auth/login (POST)', () => {
  return request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
    .expect(200)
})


// 如果用戶不存在,應(yīng)該拋出 401 異常
it('/auth/login (POST) with user not found', () => {
  return request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
    .expect(401) // Expect an unauthorized error
})

e2e 的測試代碼寫起來比較簡單,直接調(diào)用接口,然后驗(yàn)證結(jié)果就可以了。比如登錄成功測試,我們只要驗(yàn)證返回結(jié)果是否是 200 即可。

前面四個(gè)測試都比較簡單,現(xiàn)在我們看一個(gè)稍微復(fù)雜點(diǎn)的 e2e 測試,即驗(yàn)證賬戶是否被鎖定。

it('/auth/login (POST) account lock after multiple failed attempts', async () => {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile()


  const app = moduleFixture.createNestApplication()
  await app.init()


  const registerResponse = await request(app.getHttpServer())
    .post('/auth/register')
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })


  const accessToken = registerResponse.body.access_token
  const maxLoginAttempts = 3 // lock user when the third try is failed


  for (let i = 0; i < maxLoginAttempts; i++) {
    await request(app.getHttpServer())
      .post('/auth/login')
      .send({ username: TEST_USER_NAME2, password: 'InvalidPassword' })
  }


  // The account is locked after the third failed login attempt
  await request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
    .then((res) => {
      expect(res.body.message).toContain(
        'The account is locked. Please try again in 5 minutes.',
      )
    })


  await request(app.getHttpServer())
    .delete('/auth/delete-user')
    .set('Authorization', `Bearer ${accessToken}`)


  await app.close()
})

當(dāng)用戶連續(xù)三次登錄失敗的時(shí)候,賬戶就會(huì)被鎖定。所以在這個(gè)測試?yán)?,我們不能使用測試賬號(hào) TEST_USER_NAME,因?yàn)闇y試成功的話這個(gè)賬戶就會(huì)被鎖定,無法繼續(xù)進(jìn)行下面的測試了。我們需要再注冊一個(gè)新用戶 TEST_USER_NAME2,專門用來測試賬戶鎖定,測試成功后再刪除這個(gè)用戶。所以你可以看到這個(gè) e2e 測試的代碼非常多,需要做大量的前置、后置工作,其實(shí)真正的測試代碼就這幾行:

// 連續(xù)三次登錄
for (let i = 0; i < maxLoginAttempts; i++) {
  await request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME2, password: 'InvalidPassword' })
}


// 測試賬號(hào)是否被鎖定
await request(app.getHttpServer())
  .post('/auth/login')
  .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
  .then((res) => {
    expect(res.body.message).toContain(
      'The account is locked. Please try again in 5 minutes.',
    )
  })

可以看到編寫 e2e 測試代碼還是相對比較簡單的,不需要考慮 mock 數(shù)據(jù),不需要考慮測試覆蓋率,只要整個(gè)系統(tǒng)流程的運(yùn)轉(zhuǎn)情況符合預(yù)期就可以了。

應(yīng)不應(yīng)該寫測試

如果有條件的話,我是比較建議大家寫測試的。因?yàn)閷憸y試可以提高系統(tǒng)的健壯性、可維護(hù)性和開發(fā)效率。

提高系統(tǒng)健壯性

我們一般編寫代碼時(shí),會(huì)關(guān)注于正常輸入下的程序流程,確保核心功能正常運(yùn)作。但是一些邊緣情況,比如異常的輸入,這些我們可能會(huì)經(jīng)常忽略掉。但當(dāng)我們開始編寫測試時(shí),情況就不一樣了,這會(huì)逼迫你去考慮如何處理并提供相應(yīng)的反饋,從而避免程序崩潰??梢哉f寫測試實(shí)際上是在間接地提高系統(tǒng)健壯性。

提高可維護(hù)性

當(dāng)你接手一個(gè)新項(xiàng)目時(shí),如果項(xiàng)目包含完善的測試,那將會(huì)是一件很幸福的事情。它們就像是項(xiàng)目的指南,幫你快速把握各個(gè)功能點(diǎn)。只看測試代碼就能夠輕松地了解每個(gè)功能的預(yù)期行為和邊界條件,而不用你逐行的去查看每個(gè)功能的代碼。

提高開發(fā)效率

想象一下,一個(gè)長時(shí)間未更新的項(xiàng)目突然接到了新需求。改了代碼后,你可能會(huì)擔(dān)心引入 bug,如果沒有測試,那就需要重新手動(dòng)測試整個(gè)項(xiàng)目——浪費(fèi)時(shí)間,效率低下。而有了完整的測試,一條命令就能得知代碼更改有沒有影響現(xiàn)有功能。即使出錯(cuò)了,也能夠快速定位,找到問題點(diǎn)。

什么時(shí)候不建議寫測試?

短期項(xiàng)目、需求迭代非??斓捻?xiàng)目不建議寫測試。比如某些活動(dòng)項(xiàng)目,活動(dòng)結(jié)束就沒用了,這種項(xiàng)目就不需要寫測試。另外,需求迭代非??斓捻?xiàng)目也不要寫測試,我剛才說寫測試能提高開發(fā)效率是有前提條件的,就是功能迭代比較慢的情況下,寫測試才能提高開發(fā)效率。如果你的功能今天剛寫完,隔一兩天就需求變更了要改功能,那相關(guān)的測試代碼都得重寫。所以干脆就別寫了,靠團(tuán)隊(duì)里的測試人員測試就行了,因?yàn)閷憸y試是非常耗時(shí)間的,沒必要自討苦吃。

根據(jù)我的經(jīng)驗(yàn)來看,國內(nèi)的絕大多數(shù)項(xiàng)目(尤其是政企類項(xiàng)目,這種項(xiàng)目你說要寫測試我都想笑)都是沒有必要寫測試的,因?yàn)樾枨蟮?,還老是推翻之前的需求,代碼都得加班寫,那有閑情逸致寫測試。

總結(jié)

在細(xì)致地講解了如何為 Nestjs 項(xiàng)目編寫單元測試及 e2e 測試之后,我還是想重申一下測試的重要性,它能夠提高系統(tǒng)的健壯性、可維護(hù)性和開發(fā)效率。如果沒有機(jī)會(huì)寫測試,我建議大家可以自己搞個(gè)練習(xí)項(xiàng)目來寫,或者說參加一些開源項(xiàng)目,給這些項(xiàng)目貢獻(xiàn)代碼,因?yàn)殚_源項(xiàng)目對于代碼要求一般都比較嚴(yán)格。貢獻(xiàn)代碼可能需要編寫新的測試用例或修改現(xiàn)有的測試用例。

參考資料

NestJS[14]: A framework for building efficient, scalable Node.js server-side applications.

MongoDB[15]: A NoSQL database used for data storage.

Jest[16]: A testing framework for JavaScript and TypeScript.

Supertest[17]: A library for testing HTTP servers.

References

[1] nestjs-interview-demo: https://github.com/woai3c/nestjs-interview-demo

[2] 帶你入門前端工程: https://woai3c.github.io/introduction-to-front-end-engineering/

[3] 從零開始實(shí)現(xiàn)一個(gè)玩具版瀏覽器渲染引擎: https://github.com/woai3c/Front-end-articles/issues/44

[4] 手把手教你寫一個(gè)簡易的微前端框架: https://github.com/woai3c/Front-end-articles/issues/31

[5] 前端監(jiān)控 SDK 的一些技術(shù)要點(diǎn)原理分析: https://github.com/woai3c/Front-end-articles/issues/26

[6] 可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析: https://github.com/woai3c/Front-end-articles/issues/19

[7] 可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析(二): https://github.com/woai3c/Front-end-articles/issues/20

[8] 可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析(三): https://github.com/woai3c/Front-end-articles/issues/21

[9] 可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析(四): https://github.com/woai3c/Front-end-articles/issues/33

[10] 低代碼與大語言模型的探索實(shí)踐: https://github.com/woai3c/Front-end-articles/issues/45

[11] 前端性能優(yōu)化 24 條建議(2020): https://github.com/woai3c/Front-end-articles/blob/master/performance.md

[12] 手把手教你寫一個(gè)腳手架: https://github.com/woai3c/Front-end-articles/issues/22

[13] 手把手教你寫一個(gè)腳手架(二): https://github.com/woai3c/Front-end-articles/issues/23

[14] NestJS: https://nestjs.com/

[15] MongoDB: https://www.mongodb.com/

[16] Jest: https://jestjs.io/

[17] Supertest: https://github.com/visionmedia/supertest

責(zé)任編輯:武曉燕 來源: 前端編程技術(shù)分享
相關(guān)推薦

2021-08-02 12:04:39

測試測試框架Cypress

2017-01-14 23:42:49

單元測試框架軟件測試

2018-06-07 13:17:12

契約測試單元測試API測試

2021-06-18 06:48:54

前端Nest.js技術(shù)熱點(diǎn)

2017-03-22 11:32:17

Node.js單元測試

2017-02-23 15:59:53

測試MockSetup

2020-08-18 08:10:02

單元測試Java

2013-06-14 09:41:07

網(wǎng)絡(luò)規(guī)劃工程外包華為

2011-04-18 13:20:40

單元測試軟件測試

2020-09-30 08:08:15

單元測試應(yīng)用

2017-03-28 12:25:36

2017-01-14 23:26:17

單元測試JUnit測試

2017-01-16 12:12:29

單元測試JUnit

2020-12-09 14:13:37

人工智能機(jī)器學(xué)習(xí)技術(shù)

2011-08-11 13:02:43

Struts2Junit

2011-06-20 17:25:02

單元測試

2017-09-10 17:41:39

React全家桶單元測試前端測試

2017-03-23 16:02:10

Mock技術(shù)單元測試

2022-03-18 21:51:10

Nest.jsAOP 架構(gòu)后端

2011-05-16 16:52:09

單元測試徹底測試
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)