Nó xảy ra cho tất cả chúng ta. Khi các dự án phần mềm phát triển, các phần của cơ sở mã kết thúc trong quá trình sản xuất mà không có bộ thử nghiệm toàn diện. Khi bạn nhìn lại cùng một vùng mã sau vài tháng, có thể khó hiểu; thậm chí tệ hơn, có thể có một lỗi và chúng tôi không biết bắt đầu sửa nó từ đâu.
Sửa đổi mã mà không có kiểm tra là một thách thức lớn. Chúng tôi không thể chắc chắn liệu chúng tôi có vi phạm bất kỳ điều gì trong quá trình này hay không và việc kiểm tra mọi thứ theo cách thủ công, tốt nhất là dễ xảy ra sai sót; thông thường, điều đó là không thể.
Xử lý loại mã này là một trong những nhiệm vụ phổ biến nhất mà chúng tôi thực hiện với tư cách là nhà phát triển và nhiều kỹ thuật đã tập trung vào vấn đề này trong nhiều năm, chẳng hạn như kiểm tra đặc tính, mà chúng tôi đã thảo luận trong một bài viết trước.
Hôm nay, chúng ta sẽ đề cập đến một kỹ thuật khác dựa trên các bài kiểm tra đặc tính 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 nên gọi nó là "test &&commit || revert". Hãy xem tại sao.
Kỹ thuật này mô tả một quy trình làm việc để kiểm tra mã kế thừa. Chúng tôi sẽ sử dụng một tập lệnh sẽ chạy các bài kiểm tra mỗi khi chúng tôi lưu các tệp dự án của mình. Quy trình như sau:
- Đầu tiên, chúng tôi tạo một bài kiểm tra đơn vị trống cho phần mã kế thừa mà chúng tôi muốn kiểm tra.
- Sau đó, chúng tôi thêm một xác nhận duy nhất và lưu thử nghiệm.
- Vì chúng tôi đã thiết lập tập lệnh của mình, nên quá trình kiểm tra sẽ tự động chạy. Nếu nó thành công, sự thay đổi được cam kết. Nếu không thành công, thay đổi sẽ bị xóa (hoàn nguyên) và chúng tôi cần thử lại.
Sau khi thử nghiệm vượt qua, chúng tôi có thể thêm một trường hợp thử nghiệm mới.
Về cơ bản, TCR là giữ cho mã của bạn ở trạng thái "xanh" thay vì viết một bài kiểm tra thất bại trước (màu đỏ) và sau đó làm cho nó vượt qua (màu xanh lá cây), như chúng tôi làm với phát triển theo hướng kiểm tra. Nếu chúng tôi viết một bài kiểm tra không đạt, nó sẽ biến mất và chúng tôi sẽ được đưa trở lại trạng thái "xanh" một lần nữa.
Mục đích
Mục tiêu chính của kỹ thuật này là hiểu mã tốt hơn một chút mỗi khi bạn thêm trường hợp thử nghiệm. Điều này tự nhiên sẽ tăng phạm vi kiểm tra và bỏ chặn nhiều tái cấu trúc mà nếu không, sẽ không thể thực hiện được.
Một trong những lợi thế của TCR là nó hữu ích trong nhiều trường hợp. Chúng tôi có thể sử dụng nó với mã không có kiểm tra nào cả hoặc với mã được kiểm tra một phần. Nếu bài kiểm tra không vượt qua, chúng tôi chỉ cần hoàn nguyên thay đổi và thử lại.
Chúng tôi có thể sử dụng nó như thế nào?
Kent Beck cho thấy, trong các bài báo và video khác nhau (được liên kết ở cuối), rằng một cách tiếp cận tốt là sử dụng một tập lệnh chạy sau khi một số tệp nhất định trong dự án được lưu.
Điều này sẽ phụ thuộc nhiều vào dự án mà bạn đang cố gắng thử nghiệm. Một cái gì đó giống như tập lệnh sau, được thực thi mỗi khi chúng tôi lưu tệp bằng plugin trong trình chỉnh sửa, là một khởi đầu tốt:
(rspec && git commit -am "WIP") || git reset --hard
Nếu bạn đang sử dụng Visual Studio Code, một plugin tốt để thực thi trên mỗi lần lưu là "runonsave". Bạn có thể bao gồm lệnh trên hoặc lệnh tương tự cho dự án của mình. Trong trường hợp này, toàn bộ tệp cấu hình sẽ là
{
"folders": [{ "path": "." }],
"settings": {
"emeraldwalk.runonsave": {
"commands": [
{
"match": "*.rb",
"cmd": "cd ${workspaceRoot} && rspec && git commit -am WIP || git reset --hard"
}
]
}
}
}
Hãy nhớ rằng sau này, bạn có thể hoàn thành cam kết với Git trực tiếp trong dòng lệnh hoặc khi hợp nhất PR nếu bạn đang sử dụng Github:
.
Điều này có nghĩa là chúng tôi sẽ chỉ nhận được một cam kết trong nhánh chính cho tất cả các cam kết mà chúng tôi đã thực hiện trên nhánh mà chúng tôi đang làm việc. Sơ đồ này từ Github giải thích rõ điều đó:
.
Viết thử nghiệm đầu tiên của chúng tôi với TCR
Chúng tôi sẽ sử dụng một ví dụ đơn giản để minh họa kỹ thuật này. Chúng tôi có một lớp mà chúng tôi biết là đang hoạt động, nhưng chúng tôi cần sửa đổi nó.
Chúng tôi chỉ có thể thực hiện thay đổi và triển khai các thay đổi. Tuy nhiên, chúng tôi muốn đảm bảo rằng chúng tôi không vi phạm bất kỳ điều gì trong quá trình này, điều này luôn là một ý kiến hay.
# 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 tiên sẽ là tạo một tệp mới cho các bài kiểm tra, vì vậy chúng tôi có thể bắt đầu thêm chúng vào đó. Chúng tôi đã thấy dòng đầu tiên trong can_retire?
phương pháp với
def can_retire?
return true if @age >= 67
...
...
end
Do đó, chúng tôi có thể kiểm tra 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
Đây là một mẹo nhanh:khi bạn đang làm việc với TCR, mỗi khi bạn lưu, các thay đổi mới nhất sẽ biến mất nếu các bài kiểm tra không vượt qua. Do đó, chúng tôi muốn có càng nhiều mã càng tốt để "thiết lập" thử nghiệm trước khi thực sự viết và lưu dòng hoặc các dòng có khẳng định.
Nếu chúng ta lưu tệp ở trên như vậy, thì chúng ta có thể thêm một dòng để kiểm tra.
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
Khi chúng tôi lưu, nếu dòng mới không biến mất, chúng tôi đã làm rất tốt; kiểm tra vượt qua!
Thêm nhiều thử nghiệm khác
Khi chúng tôi có thử nghiệm đầu tiên, chúng tôi có thể tiếp tục bổ sung thêm các trường hợp khác trong khi tính đến các trường hợp sai. Sau một số công việc, chúng tôi có một cái gì đó như thế này:
# 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 tôi viết khối "it" trước, lưu, sau đó thêm xác nhận với expect(...)
.
Như thường lệ, chúng tôi có thể thêm nhiều thử nghiệm nhất có thể, nhưng bạn nên tránh thêm quá nhiều khi chúng tôi tương đối chắc chắn rằng mọi thứ đã được hoàn thiện.
Vẫn còn một số trường hợp cần đề cập, vì vậy chúng tôi nên thêm chúng để hoàn chỉnh.
Bài kiểm tra cuối cùng
Đây là tệp thông số kỹ thuật ở dạng cuối cùng của nó. Như bạn có thể thấy, chúng tôi vẫn có thể thêm nhiều trường hợp hơn, nhưng tôi nghĩ điều này là đủ để minh họa quá 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ác cách để Refactor
Nếu bạn đã đọc đến đây, có lẽ có điều gì đó hơi khó hiểu với mã. Chúng tôi có nhiều "số kỳ diệu" cần được trích xuất thành hằng số, cả trong bài kiểm tra và trong lớp Công nhân.
Chúng tôi cũng có thể tạo các phương thức riêng tư cho từng trường hợp trong can_retire chính? phương pháp công khai.
Tôi sẽ để lại cả hai cách tái cấu trúc tiềm năng làm bài tập cho bạn. Tuy nhiên, chúng tôi hiện có các bài kiểm tra, vì vậy nếu chúng tôi mắc lỗi ở bất kỳ bước nào, họ sẽ cho chúng tôi biết.
Kết luận
Tôi khuyến khích bạn thử TCR với các dự án của mình. Đó là một thử nghiệm rất rẻ vì bạn không cần bất kỳ tích hợp liên tục ưa thích nào trong một máy chủ bên ngoài hoặc phụ thuộc vào một thư viện mới. Tất cả những gì bạn cần là cách thực thi lệnh mỗi khi lưu một số tệp nhất định trên máy tính của mình.
Nó cũng sẽ cung cấp cho bạn trải nghiệm "chơi game" khi thêm các bài kiểm tra, điều này luôn thú vị và hấp dẫn. Ngoài ra, kỷ luật về việc xóa các bài kiểm tra không đạt khỏi trình soạn thảo của bạn sẽ cung cấp cho bạn một mạng lưới an toàn bổ sung bằng cách xác nhận rằng các bài kiểm tra bạn đang đẩy vào kho lưu trữ đang vượt qua.
Tôi hy vọng bạn thấy kỹ thuật mới này hữu ích khi xử lý mã kế thừa. Tôi đã sử dụng nhiều lần trong vài tháng qua và điều đó luôn rất vui.
Tài nguyên bổ sung
- Video hay như một phần giới thiệu.
- Kent Beck hướng dẫn cách sử dụng TCR trong VS Code.
- Trình cắm của VS Code để chạy một tập lệnh khi lưu.