avatar
threadsinstagram

[Closures][Easy] To Be Or Not To Be (手寫 expect 函式)

Table of Contents

🔸 題目描述

LeetCode 30 Days of JavaScript - 2704. To Be Or Not To Be

為了幫助開發人員測試程式碼,你需要撰寫一個 expect 的函式。該函式可以接受任何值 val,並返回一個包含以下兩個功能的物件:

  • toBe(val) 接受另一個值,如果這兩個值相等(即 ===),則返回 true。如果不相等,則會拋出一個錯誤訊息 "Not Equal"。
  • notToBe(val) 接受另一個值,如果這兩個值不相等(即 !==),則返回 true。如果相等,則會拋出一個錯誤訊息 "Equal"。
// 範例一
輸入: func = () => expect(5).toBe(5)
輸出: {"value": true}
解說: 5 === 5 so this expression returns true.
 
// 範例二
輸入: func = () => expect(5).toBe(null)
輸出: {"error": "Not Equal"}
解說: 5 !== null so this expression throw the error "Not Equal".

💭 分析與思路

問題釐清

  • 應該是比照 testing 函式庫,toBenotToBe 的實作只需比對 primitive values 就好?

提出測試案例

  • 分別驗證 toBe、notToBe 的正反面測資

提出思路

  • 題目蠻基本的,因為是 primitive values 的比對,所以就是在 function 內 return 一個物件包含有 toBenotToBe 函式分別實作比對與拋錯的邏輯就完成了

實作

const customExpect = <T>(val: T) => {
  return {
    toBe: (expected: T) => {
      if (val === expected) {
        return true;
      } else {
        throw new Error('Not Equal');
      }
    },
    notToBe: (expected: T) => {
      if (val !== expected) {
        return true;
      } else {
        throw new Error('Equal');
      }
    },
  };
};

在寫單元測試時發現如果要比對 customExpect(5).toBe(null) 時其實就會噴錯,因為在定義型別時就有期待 val 與 expected 都是同型別

但如果分開來寫:

const customExpect = <T, E>(val: T) => {
  return {
    toBe: (expected: E) => {
      if (val === expected) {
        return true;
      } else {
        throw new Error('Not Equal');
      }
    },
    notToBe: (expected: E) => {
      if (val !== expected) {
        return true;
      } else {
        throw new Error('Equal');
      }
    },
  };
};

此時 TypeScript 本身也會檢查型別的相等的合理性而噴錯:

This comparison appears to be unintentional because the types ‘T' and 'E' have no overlap.

除非是改成這樣,但想想其實也有點多此一舉,因為如果是只拿來做 primitive value 的比對,理論上型別本身就要一樣,不需要寫成兩個泛型:

const customExpect = <T, E extends T>(val: T) => {
  ...
};

也補個基本的單元測試:

import { describe, expect, test } from 'vitest';
import customExpect from './customExpect';
 
const testCases = [
  {
    title: 'toBe - positive case',
    assert: () => customExpect(5).toBe(5),
  },
  {
    title: 'toBe - negative case',
    assert: () => customExpect(5).toBe(10),
    expectedError: 'Not Equal',
  },
  {
    title: 'notToBe - positive case',
    assert: () => customExpect(5).notToBe(10),
  },
  {
    title: 'notToBe - negative case',
    assert: () => customExpect(5).notToBe(5),
    expectedError: 'Equal',
  },
];
 
describe('custom expect', () => {
  test.each(testCases)('$title', ({ assert, expectedError }) => {
    try {
      expect(assert()).toBe(true);
    } catch (error) {
      if (error instanceof Error) {
        expect(error.message).toBe(expectedError);
      }
    }
  });
});