มันเกิดขึ้นกับพวกเราทุกคน เมื่อโปรเจ็กต์ซอฟต์แวร์เติบโตขึ้น บางส่วนของโค้ดเบสก็สิ้นสุดลงในการผลิตโดยไม่มีชุดทดสอบที่ครอบคลุม เมื่อคุณลองพิจารณาโค้ดเดิมอีกครั้งหลังจากผ่านไปสองสามเดือน มันอาจจะเข้าใจยาก ที่แย่ไปกว่านั้นคือ อาจมีข้อบกพร่อง และเราไม่รู้ว่าจะเริ่มต้นแก้ไขตรงไหน
การปรับเปลี่ยนโค้ดโดยไม่ทำการทดสอบถือเป็นความท้าทายที่สำคัญ เราไม่สามารถแน่ใจได้ว่าเราจะทำลายสิ่งใดในกระบวนการ และการตรวจสอบทุกอย่างด้วยตนเองอย่างดีที่สุดมีแนวโน้มที่จะเกิดข้อผิดพลาด ปกติมันเป็นไปไม่ได้
การจัดการกับโค้ดประเภทนี้เป็นงานทั่วไปอย่างหนึ่งที่เราทำในฐานะนักพัฒนา และเทคนิคมากมายที่เน้นไปที่ปัญหานี้ในช่วงหลายปีที่ผ่านมา เช่น การทดสอบการกำหนดลักษณะที่เราได้พูดคุยกันในบทความที่แล้ว
วันนี้ เราจะมาพูดถึงเทคนิคอื่นโดยอิงจากการทดสอบลักษณะเฉพาะและแนะนำโดย Kent Beck ผู้ซึ่งได้แนะนำ TDD ให้กับโลกของการเขียนโปรแกรมสมัยใหม่เมื่อหลายปีก่อน
TCR คืออะไร
TCR ย่อมาจาก "test, commit, revert" แต่เรียกมันว่า "test &&commit || revert" ได้แม่นยำกว่า มาดูกันว่าทำไม
เทคนิคนี้อธิบายเวิร์กโฟลว์เพื่อทดสอบโค้ดเดิม เราจะใช้สคริปต์ที่จะทำการทดสอบทุกครั้งที่เราบันทึกไฟล์โครงการของเรา ขั้นตอนมีดังนี้:
- ขั้นแรก เราสร้างการทดสอบหน่วยว่างสำหรับส่วนของรหัสเดิมที่เราต้องการทดสอบ
- จากนั้นเราจะเพิ่มการยืนยันเพียงครั้งเดียวและบันทึกการทดสอบ
- เนื่องจากเราได้ตั้งค่าสคริปต์แล้ว การทดสอบจึงทำงานโดยอัตโนมัติ หากทำได้สำเร็จ การเปลี่ยนแปลงจะเกิดขึ้น หากไม่สำเร็จ การเปลี่ยนแปลงจะถูกลบ (เปลี่ยนกลับ) และเราจำเป็นต้องลองอีกครั้ง
เมื่อการทดสอบผ่าน เราก็สามารถเพิ่มกรณีทดสอบใหม่ได้
โดยพื้นฐานแล้ว TCR นั้นเกี่ยวกับการรักษาโค้ดของคุณให้อยู่ในสถานะ "สีเขียว" แทนที่จะเขียนการทดสอบที่ล้มเหลวก่อน (สีแดง) แล้วจึงทำให้มันผ่าน (สีเขียว) เช่นเดียวกับที่เราทำกับการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ หากเราเขียนการทดสอบที่ล้มเหลว การทดสอบนั้นจะหายไป และเราจะถูกนำกลับสู่สถานะ "สีเขียว" อีกครั้ง
วัตถุประสงค์
เป้าหมายหลักของเทคนิคนี้คือทำความเข้าใจโค้ดให้ดีขึ้นเล็กน้อยทุกครั้งที่คุณเพิ่มกรณีทดสอบ การทำเช่นนี้จะเพิ่มความครอบคลุมในการทดสอบโดยธรรมชาติและเลิกบล็อกการปรับโครงสร้างใหม่จำนวนมากที่ไม่เช่นนั้นจะไม่สามารถทำได้
ข้อดีอย่างหนึ่งของ TCR คือมีประโยชน์ในหลายสถานการณ์ เราสามารถใช้กับรหัสที่ไม่มีการทดสอบเลยหรือกับรหัสที่ทดสอบแล้วบางส่วน หากการทดสอบไม่ผ่าน เราก็แค่ยกเลิกการเปลี่ยนแปลงแล้วลองอีกครั้ง
เราจะใช้งานได้อย่างไร
Kent Beck แสดงให้เห็นในบทความและวิดีโอต่างๆ (ลิงก์ในตอนท้าย) ว่าแนวทางที่ดีคือการใช้สคริปต์ที่ทำงานหลังจากบันทึกไฟล์บางไฟล์ในโครงการแล้ว
สิ่งนี้จะขึ้นอยู่กับโครงการที่คุณพยายามทดสอบเป็นอย่างมาก บางสิ่งเช่นสคริปต์ต่อไปนี้ ซึ่งทำงานทุกครั้งที่เราบันทึกไฟล์ด้วยปลั๊กอินในตัวแก้ไข ถือเป็นการเริ่มต้นที่ดี:
(rspec && git commit -am "WIP") || git reset --hard
หากคุณกำลังใช้ Visual Studio Code ปลั๊กอินที่ดีในการดำเนินการในทุกการบันทึกคือ "runonsave" คุณสามารถรวมคำสั่งด้านบนหรือคำสั่งที่คล้ายกันสำหรับโครงการของคุณ ในกรณีนี้ ไฟล์ปรับแต่งทั้งหมดจะเป็น
{
"folders": [{ "path": "." }],
"settings": {
"emeraldwalk.runonsave": {
"commands": [
{
"match": "*.rb",
"cmd": "cd ${workspaceRoot} && rspec && git commit -am WIP || git reset --hard"
}
]
}
}
}
จำไว้ว่าในภายหลัง คุณสามารถสควอชคอมมิตด้วย Git ได้โดยตรงในบรรทัดคำสั่ง หรือเมื่อรวม PR หากคุณใช้ Github:
.
ซึ่งหมายความว่าเราจะได้คอมมิชชันเดียวใน main branch สำหรับคอมมิชชันทั้งหมดที่เราทำบน branch ที่เรากำลังดำเนินการอยู่ แผนภาพนี้จาก Github อธิบายได้ดี:
.
เขียนการทดสอบครั้งแรกของเราด้วย TCR
เราจะใช้ตัวอย่างง่ายๆ เพื่อแสดงเทคนิค เรามีคลาสที่รู้ว่าใช้งานได้ แต่เราต้องแก้ไข
เราสามารถทำการเปลี่ยนแปลงและปรับใช้การเปลี่ยนแปลงได้ อย่างไรก็ตาม เราต้องการให้แน่ใจว่าเราจะไม่ทำลายสิ่งใดในกระบวนการ ซึ่งเป็นความคิดที่ดีเสมอ
# worker.rb
class Worker
def initialize(age, active_years, veteran)
@age = age
@active_years = active_years
@veteran = veteran
end
def can_retire?
return true if @age >= 67
return true if @active_years >= 30
return true if @age >= 60 && @active_years >= 25
return true if @veteran && @active_years > 25
false
end
end
ขั้นตอนแรกคือการสร้างไฟล์ใหม่สำหรับการทดสอบ เพื่อให้เราสามารถเริ่มเพิ่มเข้าไปที่นั่นได้ เราได้เห็นบรรทัดแรกใน can_retire?
วิธีการด้วย
def can_retire?
return true if @age >= 67
...
...
end
ดังนั้น เราสามารถทดสอบกรณีนี้ก่อน:
# specs/worker_spec.rb
require_relative './../worker'
describe Worker do
describe 'can_retire?' do
it "should return true if age is higher than 67" do
end
end
end
คำแนะนำโดยย่อ:เมื่อคุณทำงานกับ TCR ทุกครั้งที่คุณบันทึก การเปลี่ยนแปลงล่าสุดจะหายไปหากการทดสอบไม่ผ่าน ดังนั้นเราจึงต้องการมีโค้ดให้มากที่สุดเพื่อ "ตั้งค่า" การทดสอบก่อนที่จะเขียนและบันทึกบรรทัดหรือบรรทัดด้วยการยืนยันจริง ๆ
หากเราบันทึกไฟล์ด้านบนแบบนั้น เราก็สามารถเพิ่มบรรทัดสำหรับการทดสอบได้
require_relative './../worker'
describe Worker do
describe 'can_retire?' do
it "should return true if age is higher than 67" do
expect(Worker.new(70, 10, false).can_retire?).to be_true ## This line can disappear when we save now
end
end
end
เมื่อเราบันทึก หากบรรทัดใหม่ไม่หายไป แสดงว่าเราทำได้ดีแล้ว การทดสอบผ่านไป!
กำลังเพิ่มการทดสอบ
เมื่อเราได้การทดสอบครั้งแรกแล้ว เราก็สามารถเพิ่มกรณีต่างๆ ได้มากขึ้นโดยคำนึงถึงกรณีเท็จด้วย หลังเลิกงาน เราก็มีแบบนี้:
# frozen_string_literal: true
require_relative './../worker'
describe Worker do
describe 'can_retire?' do
it 'should return true if age is higher than 67' do
expect(Worker.new(70, 10, false).can_retire?).to be true
end
it 'should return true if age is 67' do
expect(Worker.new(67, 10, false).can_retire?).to be true
end
it 'should return true if age is less than 67' do
expect(Worker.new(50, 10, false).can_retire?).to be false
end
it 'should return true if active years is higher than 30' do
expect(Worker.new(60, 31, false).can_retire?).to be true
end
it 'should return true if active years is 30' do
expect(Worker.new(60, 30, false).can_retire?).to be true
end
end
end
ในทุกกรณี เราเขียนบล็อก "it" ก่อน บันทึก จากนั้นเพิ่มการยืนยันด้วย expect(...)
.
ตามปกติแล้ว เราสามารถเพิ่มการทดสอบได้มากเท่าที่เป็นไปได้ แต่ควรหลีกเลี่ยงการเพิ่มมากเกินไปเมื่อเราค่อนข้างแน่ใจว่าครอบคลุมทุกอย่างแล้ว
ยังมีบางกรณีที่ต้องกล่าวถึง ดังนั้นเราจึงควรเพิ่มเพื่อความสมบูรณ์
การทดสอบครั้งสุดท้าย
นี่คือไฟล์ข้อมูลจำเพาะในรูปแบบสุดท้าย อย่างที่คุณเห็น เรายังสามารถเพิ่มกรณีอื่นๆ ได้ แต่ฉันคิดว่านี่เพียงพอที่จะแสดงให้เห็นกระบวนการของ TCR
# frozen_string_literal: true
require_relative './../worker'
describe Worker do
describe 'can_retire?' do
it 'should return true if age is higher than 67' do
expect(Worker.new(70, 10, false).can_retire?).to be true
end
it 'should return true if age is 67' do
expect(Worker.new(67, 10, false).can_retire?).to be true
end
it 'should return true if age is less than 67' do
expect(Worker.new(50, 10, false).can_retire?).to be false
end
it 'should return true if active years is higher than 30' do
expect(Worker.new(60, 31, false).can_retire?).to be true
end
it 'should return true if active years is 30' do
expect(Worker.new(20, 30, false).can_retire?).to be true
end
it 'should return true if age is higher than 60 and active years is higher than 25' do
expect(Worker.new(60, 30, false).can_retire?).to be true
end
it 'should return true if age is higher than 60 and active years is higher than 25' do
expect(Worker.new(61, 30, false).can_retire?).to be true
end
it 'should return true if age is 60 and active years is higher than 25' do
expect(Worker.new(60, 30, false).can_retire?).to be true
end
it 'should return true if age is higher than 60 and active years is 25' do
expect(Worker.new(61, 25, false).can_retire?).to be true
end
it 'should return true if age is 60 and active years is 25' do
expect(Worker.new(60, 25, false).can_retire?).to be true
end
it 'should return true if is veteran and active years is higher than 25' do
expect(Worker.new(60, 25, false).can_retire?).to be true
end
end
end
วิธีการปรับโครงสร้างใหม่
หากคุณได้อ่านมาถึงตอนนี้ อาจมีบางอย่างที่รู้สึกผิดกับโค้ด เรามี "ตัวเลขมหัศจรรย์" มากมายที่ควรแยกออกมาเป็นค่าคงที่ ทั้งในการทดสอบและในคลาสผู้ปฏิบัติงาน
เรายังสามารถสร้างวิธีการส่วนตัวสำหรับแต่ละกรณีใน can_retire หลักได้หรือไม่ วิธีสาธารณะ
ฉันจะปล่อยให้การ refactoring ที่เป็นไปได้ทั้งสองแบบเป็นแบบฝึกหัดสำหรับคุณ อย่างไรก็ตาม ขณะนี้เรามีการทดสอบ ดังนั้นหากเราทำผิดพลาดในขั้นตอนใดพวกเขาจะบอกเรา
บทสรุป
ฉันแนะนำให้คุณลองใช้ TCR กับโครงการของคุณ เป็นการทดลองที่ราคาถูกมากเพราะคุณไม่จำเป็นต้องมีการผสานรวมแบบต่อเนื่องในเซิร์ฟเวอร์ภายนอกหรือการพึ่งพาไลบรารีใหม่ สิ่งที่คุณต้องมีคือวิธีดำเนินการคำสั่งทุกครั้งที่คุณบันทึกไฟล์บางไฟล์ในคอมพิวเตอร์ของคุณ
นอกจากนี้ยังให้ประสบการณ์ "การเล่นเกม" แก่คุณเมื่อเพิ่มการทดสอบ ซึ่งสนุกและน่าสนใจอยู่เสมอ นอกจากนี้ ระเบียบวินัยในการลบการทดสอบที่ล้มเหลวออกจากตัวแก้ไขของคุณจะทำให้คุณมีเครือข่ายความปลอดภัยเพิ่มเติมโดยยืนยันว่าการทดสอบที่คุณส่งไปยังที่เก็บนั้นผ่าน
ฉันหวังว่าคุณจะพบว่าเทคนิคใหม่นี้มีประโยชน์เมื่อต้องรับมือกับโค้ดเดิม ฉันใช้หลายครั้งในช่วงสองสามเดือนที่ผ่านมา และมันก็เป็นความยินดีเสมอมา
ทรัพยากรเพิ่มเติม
- วิดีโอแนะนำที่ดี
- แนวคิดของ Kent Beck เกี่ยวกับวิธีใช้ TCR ใน VS Code
- ปลั๊กอินของ VS Code เพื่อเรียกใช้สคริปต์เมื่อบันทึก