หนึ่งในความท้าทายที่ยากที่สุดในการทำงานกับแอปพลิเคชันรุ่นเก่าบางตัวคือโค้ดไม่ได้ถูกเขียนขึ้นเพื่อให้ทดสอบได้ ดังนั้น การเขียนแบบทดสอบที่มีความหมายจึงเป็นเรื่องยากหรือเป็นไปไม่ได้ .
มันเป็นปัญหาที่เกิดขึ้นระหว่างไก่กับไข่ ในการเขียนการทดสอบสำหรับแอปพลิเคชันรุ่นเก่า คุณต้องเปลี่ยนรหัส แต่คุณไม่สามารถเปลี่ยนรหัสได้อย่างมั่นใจโดยไม่เขียนการทดสอบก่อน!
คุณจัดการกับความขัดแย้งนี้อย่างไร
นี่เป็นหนึ่งในหลาย ๆ วิชาที่ครอบคลุมใน การทำงานอย่างมีประสิทธิภาพด้วยรหัสดั้งเดิม ที่ยอดเยี่ยมของ Michael Feathers . วันนี้ผมจะขยายความเทคนิคเฉพาะจากหนังสือชื่อ Sprout Class .
เตรียมพร้อมสำหรับรหัส LEGACY!
มาดูคลาส ActiveRecord เก่านี้ที่ชื่อว่า Appointment
. มันค่อนข้างยาว และในชีวิตจริงมันยาวกว่า 100 บรรทัด
class Appointment < ActiveRecord::Base
has_many :appointment_services, :dependent => :destroy
has_many :services, :through => :appointment_services
has_many :appointment_products, :dependent => :destroy
has_many :products, :through => :appointment_products
has_many :payments, :dependent => :destroy
has_many :transaction_items
belongs_to :client
belongs_to :stylist
belongs_to :time_block_type
def record_transactions
transaction_items.destroy_all
if paid_for?
save_service_transaction_items
save_product_transaction_items
save_tip_transaction_item
end
end
def save_service_transaction_items
appointment_services.reload.each { |s| s.save_transaction_item(self.id) }
end
def save_product_transaction_items
appointment_products.reload.each { |p| p.save_transaction_item(self.id) }
end
def save_tip_transaction_item
TransactionItem.create!(
:appointment_id => self.id,
:stylist_id => self.stylist_id,
:label => "Tip",
:price => self.tip,
:transaction_item_type_id => TransactionItemType.find_or_create_by_code("TIP").id
)
end
end
การเพิ่มคุณสมบัติบางอย่าง
หากเราถูกขอให้เพิ่มฟังก์ชันการทำงานใหม่ในพื้นที่การรายงานธุรกรรม แต่ Appointment
class มีการพึ่งพามากเกินไปที่จะทดสอบได้โดยไม่ต้องมีการจัดโครงสร้างใหม่ เราจะดำเนินการอย่างไร
ทางเลือกหนึ่งคือทำการเปลี่ยนแปลง:
def record_transactions
transaction_items.destroy_all
if paid_for?
save_service_transaction_items
save_product_transaction_items
save_tip_transaction_item
send_thank_you_email_to_client # New code
end
end
def send_thank_you_email_to_client
ThankYouMailer.thank_you_email(self).deliver
end
แย่จังเลย
โค้ดด้านบนมีปัญหาสองประการ:
-
Appointment
มีความรับผิดชอบที่แตกต่างกันมากมาย (การละเมิดหลักการความรับผิดชอบเดียว) หนึ่งในความรับผิดชอบเหล่านี้คือ*การบันทึกธุรกรรม . โดยเพิ่มโค้ดที่เกี่ยวข้องกับธุรกรรมเพิ่มเติมในAppointment
class **เรากำลังทำให้โค้ดแย่ลงอีกนิด *. -
เราอาจเขียนการทดสอบการรวมระบบใหม่และตรวจสอบเพื่อดูว่าอีเมลส่งผ่านหรือไม่ แต่เนื่องจากเราจะไม่มี
Appointment
คลาสในสถานะทดสอบได้ เราไม่สามารถเพิ่มการทดสอบหน่วยใดๆ เราจะเพิ่มโค้ดที่ยังไม่ได้ทดสอบเพิ่มเติม ซึ่งแน่นอนว่าแย่ (อันที่จริง Michael Feathers กำหนด รหัสเดิม เป็น "รหัสที่ไม่มีการทดสอบ" ดังนั้นเราจะเพิ่มรหัสดั้งเดิม _more_ ลงในรหัสดั้งเดิม)
เลิกกันจะดีกว่า
ทางออกที่ดีกว่าการเพิ่มโค้ดใหม่แบบอินไลน์คือการแยกพฤติกรรมการบันทึกธุรกรรมออกเป็นคลาสของตัวเอง เราจะเรียกมันว่าTransactionRecorder
:
class TransactionRecorder
def initialize(options)
@appointment_id = options[:appointment_id]
@appointment_services = options[:appointment_services]
@appointment_products = options[:appointment_products]
@stylist_id = options[:stylist_id]
@tip = options[:tip]
end
def run
save_service_transaction_items(@appointment_services)
save_product_transaction_items(@appointment_products)
save_tip_transaction_item(@appointment_id, @stylist_id, @tip_amount)
end
def save_service_transaction_items(appointment_services)
appointment_services.each { |s| s.save_transaction_item(appointment_id) }
end
def save_product_transaction_items(appointment_products)
appointment_products.each { |p| p.save_transaction_item(appointment_id) }
end
def save_tip_transaction_item(appointment_id, stylist_id, tip)
TransactionItem.create!(
appointment_id: appointment_id,
stylist_id: stylist_id,
label: "Tip",
price: tip,
transaction_item_type_id: TransactionItemType.find_or_create_by_code("TIP").id
)
end
end
ผลตอบแทน
แล้ว Appointment
ย่อลงมาได้แค่นี้:
class Appointment < ActiveRecord::Base
has_many :appointment_services, :dependent => :destroy
has_many :services, :through => :appointment_services
has_many :appointment_products, :dependent => :destroy
has_many :products, :through => :appointment_products
has_many :payments, :dependent => :destroy
has_many :transaction_items
belongs_to :client
belongs_to :stylist
belongs_to :time_block_type
def record_transactions
transaction_items.destroy_all
if paid_for?
TransactionRecorder.new(
appointment_id: id,
appointment_services: appointment_services,
appointment_products: appointment_products,
stylist_id: stylist_id,
tip: tip
).run
end
end
end
เรายังคงแก้ไขโค้ดใน Appointment
ซึ่งไม่สามารถทดสอบได้ แต่ตอนนี้ we_can_ ทดสอบทุกอย่างใน TransactionRecorder
และเนื่องจากเราได้เปลี่ยนแต่ละฟังก์ชันให้ยอมรับอาร์กิวเมนต์แทนที่จะใช้ตัวแปรอินสแตนซ์ เราจึงสามารถทดสอบแต่ละฟังก์ชันแบบแยกส่วนได้ ตอนนี้เราอยู่ในจุดที่ดีกว่าตอนเริ่มต้นมาก