[生活筆記] 修正 A 校的題目缺陷

導言

在 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
// start coding

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());
//console.log('now random is', Math.random());
const index = require('./index');
index.getResult();

// Clean up
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());
//console.log('now random is', Math.random());
const index = require('./index');
index.getResult();

// Clean up
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(() => {
// Save the original functions
originalMathRandom = Math.random;
spy = jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
// Restore the original functions
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次了,請重新開始');

});


// Add more tests as needed for the getResult function
});

給學生的出題程式

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;

// start coding

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;

// start coding

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)

Please enable JavaScript to view the Gitalk. :D
Please enable JavaScript to view the LikeCoin. :P
Please enable JavaScript to view the LikeCoin. :P