導言
在 A 校擔任雲端助教一陣子,主要有幾個原因,
我也是資策會出身,所以我對轉職的學生可以感同身受,
我在教學的過程中,自已也會成長,很多時候可以學習到新知也得到成就感,
最後就是可以獲得額外的收入。
問題發現
這次是一個我有興趣的題目,簡單說 A 社利用 Replit 網站提供作業,
由學生進行撰寫,很聰明的整合方式,而且幾乎零成本。
而且我覺得最棒的是,他可以寫測試腳本,透過測試腳本,就可以驗收大部份的作業,加速批改的時間。
但是,這次同學的作業在執行時期,發生了異常掉入了一個無限廻圈的狀態
問題不難,是一個邊際值的問題。 應該可以加上測試保護,這是我的第一個直覺。
我去檢查了 A 校提供的標準答案,不出意外的也有相同的問題。
另外一個問題是,寫邊際值的測試案例不是基本的嗎?
查看了測試案例,竟然還真的沒有寫
問題排查
我選擇下載了 Replit 到地端開發,
在開始前先簡單描述題目,在一個限範圍內進行猜數字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const answer = Math.floor(Math.random() * 100) + 1
let min = 1 let max = 100 let guess = Math.floor(Math.random() * 100) + 1 let count = 1
function getResult() { while ( ) { if ( ) {
} else if ( ) {
} guess = Math.floor((max + min) / 2) count ++ } console.log( ) }
getResult()
module.exports = { guess, answer, count, getResult }
|
這是很好的題目,可以同時使用到迴圈與判斷,也可以接觸 Math 模組。
我們來看一下測試案例,在 Replit 右下角的 Unit Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| test("", async function() { const spy = jest.spyOn(console, 'log') const { guess, answer, count, getResult } = index;
getResult() if (guess === answer) { expect(count).toBeLessThanOrEqual(10) } });
|
測試很單純,當答案在 1~100 之間時,執行迴圈的次數不應該超過 10 次(其實 7 次內應該都猜得出來)
我們應該加上一些邊際測試。
例如 1 與 100 的案例,
1 2 3 4 5 6 7 8 9 10 11 12 13
| it("Answer_is_1", async function() { const spy = jest.spyOn(console, 'log'); const originalMathRandom = Math.random; Math.random = jest.fn() .mockImplementationOnce(() => 0.001) .mockImplementation(() => originalMathRandom()); const index = require('./index'); index.getResult();
spy.mockRestore(); });
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| it("Answer_is_100", async function() { const spy = jest.spyOn(console, 'log'); const originalMathRandom = Math.random; Math.random = jest.fn() .mockImplementationOnce(() => 0.999) .mockImplementation(() => originalMathRandom()); const index = require('./index'); index.getResult();
spy.mockRestore(); });
|
這裡注意到的是我們 mock 了Math.random
,因為這才會影響我們的答案。
問題後的問題
當我們在測試案例為 100 時,會限入無窮迴圈,而 JavaScript 單緒的特性將無法離開這個測試,
雖然也不會報錯…但是測試將永遠跑不完。
這驅使我再加上一個測試案例,當執行次數超過 10 次時拋出例外。
而學生使用的範本,我希望儘可能不去修改它,
這裡要對 jest 與 javascript 要有足夠的理解才可以寫的好,
所幸在這個時代,有 AI 與 google 的加持下,很快就解決了。
最後的測試程式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| describe('Guessing Game', () => { let originalMathRandom; let spy;
beforeEach(() => { originalMathRandom = Math.random; spy = jest.spyOn(console, 'log').mockImplementation(() => {}); });
afterEach(() => { Math.random = originalMathRandom; spy.mockRestore(); jest.resetModules() });
it("Answer_is_1", async function() { Math.random = jest.fn().mockReturnValue(0.001); const index = require('./index'); index.getResult(); expect(index.answer).toBe(1); });
it("Answer_is_100", async function() { Math.random = jest.fn().mockReturnValue(0.999); const index = require('./index'); index.getResult(); expect(index.answer).toBe(100); });
it('guess under 10 times', () => { Math.random = jest.fn().mockReturnValue(0.5); const index = require('./index'); for(let i=0; i<9; i++) { index.getResult(); } expect(() => index.getResult()).not.toThrow(); });
it('should throw error if count is more than 10', () => { const originalMathFloor = Math.floor; Math.floor = jest.spyOn(global.Math, 'floor') .mockImplementationOnce(() => Math.floor(Math.random() * 100) + 1) .mockImplementationOnce(() => 1000); const index = require('./index'); expect(() => index.getResult()).toThrow('超過10次了,請重新開始'); });
});
|
給學生的出題程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const answer = Math.floor(Math.random() * 100) + 1;
let min = 1; let max = 100; let guess = Math.floor(Math.random() * 100) + 1; let count = 1;
function getResult() { console.log(`Guess number is ${guess}`); while ( ) { if ( ) {
} else if () {
}
count++; if (count > 10) { throw new Error('超過10次了,請重新開始'); } } console.log(`第${count}回合,您猜${guess},猜對了`); }
module.exports = { guess, answer, count, getResult, };
|
解答範本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const answer = Math.floor(Math.random() * 100) + 1;
let min = 1; let max = 100; let guess = Math.floor(Math.random() * 100) + 1; let count = 1;
function getResult() { console.log(`Guess number is ${guess}`); while (guess !== answer) { if (answer < guess) { max = guess; console.log(`第${count}回合,您猜${guess},太大了,請猜介於${min}~${max}之間的數字`); } else if (answer > guess) { min = guess; console.log(`第${count}回合,您猜${guess},太小了,請猜介於${min}~${max}之間的數字`); } guess = (max === min + 1) ? max : Math.floor((max + min) / 2);
count++; if (count > 10) { throw new Error('超過10次了,請重新開始'); } } console.log(`第${count}回合,您猜${guess},猜對了`); }
module.exports = { guess, answer, count, getResult, };
|
小結
整個課程的調整與測試改寫,大概花了我 4~12 小時處理。
在改制後,互動環節變少了,計時制改為定時制的給薪,也讓無法花太多的時間幫學生排查問題。
這題目我有興趣就順手解決了。
可惜的是,我拿不到任何費用。
也再一次印証 AI 的強大,未來的人材需要有更高的整合能力,
寫測試寫程式,讀技術文章賺取資訊落的錢應該會越來越難賺。
但是能高度整合的人應該會更為搶手。
(fin)