कुछ पुराने अनुप्रयोगों के साथ काम करने की सबसे कठिन चुनौतियों में से एक यह है कि कोड को परीक्षण योग्य होने के लिए नहीं लिखा गया था। इसलिए सार्थक परीक्षण लिखना कठिन या असंभव है ।
यह चिकन और अंडे की समस्या है:लीगेसी एप्लिकेशन के लिए परीक्षण लिखने के लिए, आपको कोड बदलना होगा, लेकिन आप इसके लिए पहले परीक्षण लिखे बिना कोड को आत्मविश्वास से नहीं बदल सकते!
आप इस विरोधाभास से कैसे निपटते हैं?
यह माइकल फेदर्स के उत्कृष्ट विरासत कोड के साथ प्रभावी ढंग से कार्य करना में शामिल कई विषयों में से एक है। . आज मैं स्प्राउट क्लास . नामक पुस्तक से एक विशेष तकनीक पर ज़ूम इन करने जा रहा हूं ।
कुछ लीगेसी कोड के लिए तैयार हो जाएं!
आइए इस पुराने 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
कक्षा में बहुत अधिक निर्भरताएँ हैं जिन्हें बहुत अधिक रिफैक्टरिंग के बिना परीक्षण योग्य बनाया जा सकता है, हम कैसे आगे बढ़ते हैं?
एक विकल्प केवल परिवर्तन करना है:
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
. में लेन-देन संबंधी अधिक कोड जोड़कर क्लास, **हम कोड को थोड़ा खराब कर रहे हैं *. -
हम शायद एक नया एकीकरण परीक्षण लिख सकते हैं और यह देखने के लिए जांच कर सकते हैं कि ईमेल ने इसे पार कर लिया है, लेकिन चूंकि हमारे पास
Appointment
नहीं होगा एक परीक्षण योग्य स्थिति में कक्षा, हम कोई इकाई परीक्षण नहीं जोड़ सके। हम अधिक परीक्षण न किए गए कोड जोड़ेंगे , जो निश्चित रूप से खराब है। (माइकल फेदर्स, वास्तव में, विरासत कोड . को परिभाषित करता है "बिना परीक्षण कोड" के रूप में, इसलिए हम लीगेसी कोड में_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
. में कोड को संशोधित कर रहे हैं , जिसका परीक्षण नहीं किया जा सकता है, लेकिन अब हम TransactionRecorder
में सब कुछ परीक्षण कर सकते हैं , और चूंकि हमने इंस्टेंस वेरिएबल्स का उपयोग करने के बजाय तर्कों को स्वीकार करने के लिए प्रत्येक फ़ंक्शन को बदल दिया है, हम अलगाव में प्रत्येक फ़ंक्शन का परीक्षण भी कर सकते हैं। इसलिए जब हमने शुरुआत की थी, तब से अब हम बहुत बेहतर स्थिति में हैं।