Điều này xảy ra với tất cả chúng ta. Khi dự án phần mềm phát triển, các phần của codebase kết thúc trong production mà không có bộ test toàn diện. Khi bạn nhìn lại cùng khu vực code sau vài tháng, có thể khó hiểu; tệ hơn, có thể có bug và chúng ta không biết bắt đầu sửa từ đâu.
Sửa đổi code không có test là thách thức lớn. Chúng ta không chắc chắn liệu có làm hỏng gì trong quá trình, và kiểm tra thủ công thì, tốt nhất, dễ mắc lỗi; thường thì không thể.
Xử lý loại code này là một trong những nhiệm vụ phổ biến nhất chúng ta làm như lập trình viên, và nhiều kỹ thuật đã tập trung vào vấn đề này nhiều năm, như characterization tests, chúng ta đã thảo luận trong bài trước.
Hôm nay, chúng ta sẽ trình bày kỹ thuật khác dựa trên characterization tests và được giới thiệu bởi Kent Beck, người cũng đưa TDD vào thế giới lập trình hiện đại nhiều năm trước.
TCR là gì?
TCR là viết tắt của "test, commit, revert", nhưng chính xác hơn là gọi nó "test && commit || revert". Hãy xem tại sao.
Kỹ thuật này mô tả quy trình làm việc để test code legacy. Chúng ta sẽ dùng script chạy test mỗi khi lưu file dự án. Quy trình như sau:
- Đầu tiên, tạo unit test rỗng cho phần code legacy muốn test.
- Sau đó, thêm một assertion đơn lẻ và lưu test.
- Vì đã setup script, test tự động chạy. Nếu thành công, thay đổi được commit. Nếu thất bại, thay đổi bị xóa (revert), và chúng ta phải thử lại.
Khi test pass, chúng ta có thể thêm test case mới.
Bản chất, TCR là giữ code ở trạng thái "xanh" thay vì viết test fail trước (đỏ) rồi làm pass (xanh) như TDD. Nếu viết test fail, nó sẽ biến mất, và chúng ta quay lại trạng thái "xanh".
Mục đích
Mục tiêu chính của kỹ thuật này là hiểu code tốt hơn mỗi khi thêm test case. Điều này sẽ tự nhiên tăng độ bao phủ test và mở khóa nhiều refactoring mà, nếu không, sẽ không thể.
Một lợi thế của TCR là hữu ích trong nhiều kịch bản. Chúng ta có thể dùng cho code hoàn toàn không có test hoặc code đã test một phần. Nếu test không pass, chỉ cần revert thay đổi và thử lại.
Làm sao để dùng nó?
Kent Beck chỉ ra, trong các bài viết và video (liên kết ở cuối), cách tiếp cận tốt là dùng script chạy sau khi lưu file nhất định trong dự án.
Điều này phụ thuộc nhiều vào dự án bạn đang test. Script như sau, thực thi mỗi khi lưu file với plugin trong editor, là khởi đầu tốt:
(rspec && git commit -am "WIP") || git reset --hard
Nếu bạn dùng Visual Studio Code, plugin tốt để thực thi mỗi khi lưu là "runonsave". Bạn có thể include lệnh trên hoặc lệnh tương tự cho dự án. Trong trường hợp này, toàn bộ file config sẽ là
{ "folders": [{ "path": "." }], "settings": { "emeraldwalk.runonsave": { "commands": [ { "match": "*.rb", "cmd": "cd ${workspaceRoot} && rspec && git commit -am WIP || git reset --hard" } ] } } }
Nhớ rằng sau đó, bạn có thể squash commit với Git trực tiếp trong command line hoặc khi merge PR nếu dùng Github:
.
Điều này nghĩa là chúng ta chỉ nhận một commit trên branch main cho tất cả commit làm trên branch đang làm việc. Sơ đồ từ Github giải thích tốt:
.
Viết test đầu tiên với TCR
Chúng ta sẽ dùng ví dụ đơn giản để minh họa kỹ thuật. Chúng ta có class biết là hoạt động, nhưng cần sửa đổi.
Chúng ta có thể chỉ làm thay đổi và deploy. Tuy nhiên, chúng ta muốn chắc chắn không làm hỏng gì trong quá trình, luôn là ý tưởng tốt.
# 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
Bước đầu là tạo file mới cho test, để bắt đầu thêm test vào đó. Chúng ta đã thấy dòng đầu trong phương thức can_retire? với
def can_retire? return true if @age >= 67 ... ... end
Vì vậy, chúng ta có thể test trường hợp này trước:
# 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
Mẹo nhanh: khi làm việc với TCR, mỗi lần lưu, thay đổi mới nhất sẽ biến mất nếu test không pass. Do đó, chúng ta muốn có nhiều code "setup" test nhất có thể trước khi viết và lưu dòng/dòng assertion.
Nếu lưu file trên như vậy, sau đó có thể thêm dòng cho test.
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 ## Dòng này có thể biến mất khi lưu bây giờ end end end
Khi lưu, nếu dòng mới không biến mất, chúng ta đã làm tốt; test pass!
Thêm nhiều test hơn
Khi đã có test đầu tiên, chúng ta có thể tiếp tục thêm case hơn nữa, cân nhắc cả trường hợp false. Sau một chút công việc, chúng ta có thể có như sau:
# 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
Trong mọi trường hợp, chúng ta viết block "it" trước, lưu, rồi thêm assertion với expect(...).
Như thường lệ, chúng ta có thể thêm bao nhiêu test tùy ý, nhưng hợp lý là tránh thêm quá nhiều khi đã tương đối chắc chắn mọi thứ đã được bao phủ.
Vẫn còn vài case cần cover, nên thêm vào cho đầy đủ.
Test cuối cùng
Đây là file spec ở dạng cuối cùng. Như bạn thấy, vẫn có thể thêm case, nhưng tôi nghĩ đủ để minh họa quy trình 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
Cách refactor
Nếu bạn đọc đến đây, có thể cảm thấy có gì đó hơi sai với code. Chúng ta có nhiều "con số ma thuật" nên extract thành hằng số, cả trong test và class Worker.
Chúng ta cũng có thể tạo phương thức private cho mỗi case trong phương thức public can_retire? chính.
Tôi để cả hai refactoring tiềm năng làm bài tập cho bạn. Tuy nhiên, chúng ta đã có test, nên nếu sai bất kỳ bước nào, chúng sẽ báo cho chúng ta.
Kết luận
Tôi khuyến khích bạn thử TCR với dự án của mình. Đây là thử nghiệm rất "rẻ" vì bạn không cần continuous integration phức tạp trên server ngoài hay dependency thư viện mới. Tất cả bạn cần là cách thực thi lệnh mỗi khi lưu file nhất định trên máy.
Nó cũng mang lại trải nghiệm "game hóa" khi thêm test, luôn vui và thú vị. Ngoài ra, kỷ luật làm test fail bị xóa khỏi editor sẽ cho bạn lưới an toàn thêm bằng cách xác nhận test bạn push lên repo đang pass.
Tôi hy vọng bạn thấy kỹ thuật mới này hữu ích khi xử lý code legacy. Tôi đã dùng nhiều lần trong vài tháng qua, và luôn là niềm vui.
Tài nguyên thêm
- Video hay làm giới thiệu.
- Góc nhìn của Kent Beck về cách dùng TCR trong VS Code.
- Plugin VS Code chạy script khi lưu.