2011-05-28 8 views
5

मुझे कुछ टीडीडी अवधारणाओं के साथ कुछ मदद चाहिए। मान लें कि मेरे पास निम्नलिखित कोडअन्य विधियों को कॉल करने वाले टीडीडी विधियों के लिए सही तरीका

def execute(command) 
    case command 
    when "c" 
    create_new_character 
    when "i" 
    display_inventory 
    end 
end 

def create_new_character 
    # do stuff to create new character 
end 

def display_inventory 
    # do stuff to display inventory 
end 

अब मुझे यकीन नहीं है कि मेरे यूनिट परीक्षणों को क्या लिखना है। यदि मैं execute विधि के लिए यूनिट परीक्षण लिखता हूं तो क्या यह create_new_character और display_inventory के लिए मेरे परीक्षणों को बहुत अधिक कवर नहीं करता है? या क्या मैं उस समय गलत सामान का परीक्षण कर रहा हूं? क्या execute विधि के लिए मेरा परीक्षण केवल परीक्षण करेगा कि निष्पादन सही तरीकों से पारित किया गया है और वहां रुकें? तो क्या मुझे अधिक यूनिट परीक्षण लिखना चाहिए जो विशेष रूप से create_new_character और display_inventory का परीक्षण करते हैं?

उत्तर

6

मुझे लगता है कि टीडीडी का उल्लेख है कि प्रश्न में कोड वास्तव में मौजूद नहीं है। यदि ऐसा होता है तो आप सही टीडीडी नहीं कर रहे हैं लेकिन टीएडी (टेस्ट-आफ डेवलपमेंट), जो स्वाभाविक रूप से इस तरह के प्रश्नों की ओर जाता है। टीडीडी में हम परीक्षण के साथ शुरू करते हैं। ऐसा प्रतीत होता है कि आप कुछ प्रकार के मेनू या कमांड सिस्टम का निर्माण कर रहे हैं, इसलिए मैं इसे एक उदाहरण के रूप में उपयोग करूंगा।

describe GameMenu do 
    it "Allows you to navigate to character creation" do 
    # Assuming character creation would require capturing additional 
    # information it violates SRP (Single Responsibility Principle) 
    # and belongs in a separate class so we'll mock it out. 
    character_creation = mock("character creation") 
    character_creation.should_receive(:execute) 

    # Using constructor injection to tell the code about the mock 
    menu = GameMenu.new(character_creation) 
    menu.execute("c") 
    end 
end 

यह परीक्षण कुछ निम्नलिखित (याद रखें, सिर्फ पर्याप्त कोड परीक्षण पारित करने के लिए, कोई और अधिक) के समान कोड

class GameMenu 
    def initialize(character_creation_command) 
    @character_creation_command = character_creation_command 
    end 

    def execute(command) 
    @character_creation_command.execute 
    end 
end 

के लिए नेतृत्व अब हम अगले परीक्षण जोड़ देंगे होगा।

class GameMenu 
    def initialize(character_creation_command, inventory_command) 
    @inventory_command = inventory_command 
    end 

    def execute(command) 
    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 
end 

इस कार्यान्वयन हमारे कोड के बारे में एक प्रश्न के लिए हमें ले जाता है:

it "Allows you to display character inventory" do 
    inventory_command = mock("inventory") 
    inventory_command.should_receive(:execute) 
    menu = GameMenu.new(nil, inventory_command) 
    menu.execute("i") 
end 

इस परीक्षण चल रहा है जैसे एक कार्यान्वयन करने के लिए हमें का नेतृत्व करेंगे। अमान्य आदेश दर्ज होने पर हमारा कोड क्या करना चाहिए? एक बार जब हम उस प्रश्न का उत्तर तय कर लेंगे तो हम एक और परीक्षण लागू कर सकते हैं।

it "Raises an error when an invalid command is entered" do 
    menu = GameMenu.new(nil, nil) 
    lambda { menu.execute("invalid command") }.should raise_error(ArgumentError) 
end 

कि अब बाहर ड्राइव execute विधि

def execute(command) 
    unless ["c", "i"].include? command 
     raise ArgumentError("Invalid command '#{command}'") 
    end 

    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 

के लिए एक त्वरित परिवर्तन है कि हम परीक्षण गुजर रहा है हम एक आशय में आदेश के सत्यापन को निकालने के लिए निकालें विधि रिफैक्टरिंग उपयोग कर सकते हैं विधि प्रकट करना।

def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 

    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 

    def invalid?(command) 
    !["c", "i"].include? command 
    end 

अब हम अंततः उस बिंदु पर पहुंच गए हैं जहां हम आपके प्रश्न को संबोधित कर सकते हैं।चूंकि invalid? विधि को परीक्षण के तहत मौजूदा कोड को पुन: सक्रिय करके संचालित किया गया था, इसके लिए यूनिट परीक्षण लिखने की कोई आवश्यकता नहीं है, यह पहले से ही कवर हो चुका है और यह स्वयं पर खड़ा नहीं है। चूंकि इन्वेंट्री और कैरेक्टर कमांड का परीक्षण हमारे मौजूदा परीक्षण द्वारा नहीं किया जाता है, इसलिए उन्हें स्वतंत्र रूप से परीक्षण करने की आवश्यकता होगी।

ध्यान दें कि हमारा कोड अभी भी बेहतर हो सकता है, जबकि परीक्षण पास हो रहे हैं, इसे थोड़ा और साफ़ करने दें। सशर्त बयान एक संकेतक है कि हम ओसीपी (ओपन-क्लोज़ेड सिद्धांत) का उल्लंघन कर रहे हैं हम सशर्त तर्क को हटाने के लिए पॉलीमोर्फिज्म रिफैक्टरिंग के साथ सशर्त प्रतिस्थापन का उपयोग कर सकते हैं।

# Refactored to comply to the OCP. 
class GameMenu 
    def initialize(character_creation_command, inventory_command) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

    def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 
    @commands[command].execute 
    end 

    def invalid?(command) 
    [email protected]_key? command 
    end 
end 

अब हम वर्ग पुनर्संशोधित गया है ऐसी है कि एक अतिरिक्त आदेश बस को आदेश हमारे सशर्त तर्क के साथ-साथ invalid? विधि को बदलने के बजाय हैश एक अतिरिक्त प्रविष्टि जोड़ने के लिए हमें की आवश्यकता है।

सभी परीक्षण अभी भी पास होना चाहिए और हमने अपना काम लगभग पूरा कर लिया है। एक बार जब हम अलग-अलग आदेशों ड्राइव का परीक्षण तुम वापस इनिशियलाइज़ विधि करने के लिए जाना और इतने की तरह आदेश के लिए कुछ चूक जोड़ सकते हैं:

def initialize(character_creation_command = CharacterCreation.new, 
       inventory_command = Inventory.new) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

अंतिम परीक्षण है:

describe GameMenu do 
    it "Allows you to navigate to character creation" do 
    character_creation = mock("character creation") 
    character_creation.should_receive(:execute) 
    menu = GameMenu.new(character_creation) 
    menu.execute("c") 
    end 

    it "Allows you to display character inventory" do 
    inventory_command = mock("inventory") 
    inventory_command.should_receive(:execute) 
    menu = GameMenu.new(nil, inventory_command) 
    menu.execute("i") 
    end 

    it "Raises an error when an invalid command is entered" do 
    menu = GameMenu.new(nil, nil) 
    lambda { menu.execute("invalid command") }.should raise_error(ArgumentError) 
    end 
end 

और अंतिम GameMenu तरह लग रहा है :

class GameMenu 
    def initialize(character_creation_command = CharacterCreation.new, 
       inventory_command = Inventory.new) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

    def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 
    @commands[command].execute 
    end 

    def invalid?(command) 
    [email protected]_key? command 
    end 
end 

आशा है कि मदद करता है!

ब्रैंडन

+1

+1: टीडीडी का उत्कृष्ट उदाहरण। विस्तृत उत्तर के लिए – Johnsyweb

+0

धन्यवाद। आपने मुझे चबाने और सोचने के लिए बहुत कुछ दिया। एकमात्र चीज जो वास्तव में आपके उदाहरण के बारे में मुझे परेशान कर रही है वह यह है कि गेममेनू प्रारंभकर्ता बहुत सारे आदेश जोड़ने के बाद वास्तव में लंबा हो जाएगा। और अगर यह ट्रैक रखना है कि मेरा नया "शो मैप" कमांड सूची के नीचे 10 पैरामीटर कहता है तो इसका परीक्षण करना आसान होगा। इसके लिए कोई अच्छा समाधान? – Dty

+0

@ डीटी बिल्कुल। मैंने इसे माना था। मैंने सोचा कि इस उदाहरण के लिए यह एक बड़ा सौदा नहीं होगा, लेकिन आपने पुष्टि की है कि यह/हो सकता है। आप इससे निपटने के कुछ तरीके हैं। दिमाग में आने वाला पहला व्यक्ति register_menu_command जोड़ना है जिसे कमांड को पंजीकृत करने के लिए बाहरी रूप से बुलाया जा सकता है। दूसरा उस पैरामीटर सूची को _Builder Pattern_ के साथ प्रतिस्थापित करेगा और बस एक मेनूबिल्डर में पास होगा जो हैश उत्पन्न करता है। आप अपने परीक्षण में बिल्डर को कॉन्फ़िगर कर सकते हैं। मैं शायद बिल्डर समाधान पसंद करेंगे। – bcarlso

3

रिफैक्टरिंग पर विचार करें ताकि कोड (आपके मामले में execute) को पार्स आदेश के लिए जिम्मेदारी है कि कोड है कि कार्यों को लागू करता है से स्वतंत्र है (अर्थात, create_new_character, display_inventory)। इससे कार्यों को नकल करना और स्वतंत्र रूप से पार्सिंग कमांड का परीक्षण करना आसान हो जाता है। आपविभिन्न टुकड़ों के स्वतंत्र परीक्षण चाहते हैं।

+0

मुझे यकीन नहीं है कि मैं समझता हूं कि आपका क्या मतलब है। उदाहरण के लिए, मुझे पहले से ही आदेशों का विश्लेषण करना और कार्यों का निष्पादन स्वतंत्र है। क्या आप मुझे अपने मतलब का संक्षिप्त कोड नमूना दिखा सकते हैं? शायद वह मुझे समझने में मदद करेगा। – Dty

0

मैं create_new_character और display_inventory के लिए सामान्य परीक्षणों का निर्माण होता है, और अंत में execute परीक्षण करने के लिए, बस एक आवरण समारोह जा रहा है, यह देखना होगा कि apropriate आदेश कहा जाता है (और परिणाम प्राप्त हुआ) उम्मीदों निर्धारित किया है। ऐसा कुछ:

def test_execute 
    commands = { 
    "c" => :create_new_character, 
    "i" => :display_inventory, 
    } 
    commands.each do |string, method| 
    instance.expects(method).with().returns(:mock_return) 
    assert_equal :mock_return, instance.execute(string) 
    end 
end 
संबंधित मुद्दे