💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 27.测试驱动开发 假设你在马戏团中,一个漂亮的女孩正在空中飞人表演,她错过了抓地力并摔倒了,你希望那里有一个安全网来捉住她吗? 你必须疯了才能拒绝。 以类似的方式,可以说你正在开发软件,犯了一个错误,使用该软件的人会付出很多代价,拥有制衡能力不是一件好事,这样即使在错误发生之前就可以知道该错误。 软件已发货? 欢迎来到测试驱动开发。 在这种方法中,我们首先编写测试,然后编写足够的代码来满足测试。 通过遵循这种方法,我能够以最大的信心进行编码,并且能够知道存在安全网以防万一我做错了事,从而能够更改代码并使之更好(也称为重构)。 让我们想象一个场景。 你的任务是编写聊天机器人程序,其初始要求如图所示 * 必须有一个聊天机器人 * 一个人必须能够设定年龄和名字 * 它的问候语必须是:“你好,我叫&lt;,名字叫&gt;,我的年龄是&lt;,年龄&gt;,很高兴认识你!” 通常,要求不会像上面显示的那样精确,但是作为一名程序员,应该可以考虑一下。 现在,我们开始编写测试文件,而不是编写代码来解决任务,将其命名为 test_chat_bot.rb 并将需求放入其中,如下所示: ```rb # test_chat_bot.rb # There must be a chat bot # One must be able to set its age # One must be able to set its name # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" ``` 现在,这些要求已在我们的程序中变成了单词,我们需要将其转换为 Ruby。 几乎所有编程语言都内置了一个测试框架,Ruby 也有一个测试框架,其名称为 Minitest &lt;sup class="footnote"&gt;[ [63](#_footnotedef_63 "View footnote.") ]&lt;/sup&gt; 。 我们将在这里使用它。 为了包括 Minitest,我们添加下面突出显示的行 ```rb # test_chat_bot.rb require "minitest/autorun" # There must be a chat bot # One must be able to set its age # One must be able to set its name # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" ``` 现在我们包含了 Minitest,现在让我们编写一个测试类,如下所示 ```rb # test_chat_bot.rb require "minitest/autorun" class TestChatBot < Minitest::Test # There must be a chat bot # One must be able to set its age # One must be able to set its name # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" end ``` 完成上面显示的内容后,现在让我们为第一个测试用例编写代码,看看下面的代码 ```rb class TestChatBot < Minitest::Test # There must be a chat bot def test_there_must_be_a_chat_bot assert_kind_of ChatBot, ChatBot.new end # One must be able to set its age # One must be able to set its name # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" end ``` 上面的代码中发生了很多事情,首先我们通过编写一个测试函数 ```rb def test_there_must_be_a_chat_bot end ``` 注意此功能如何以`test_`开头,这对于将其识别为测试至关重要。 接下来,我们必须测试此功能中的某些内容。 只有在`ChatBot`类存在的情况下,我们才能使用`ChatBot`类的实例,因此我们尝试在下面的突出显示的代码中创建一个新的`ChatBot`实例,并检查其类是否为`ChatBot`。 ```rb def test_there_must_be_a_chat_bot assert_kind_of ChatBot, ChatBot.new end ``` 如果你想知道这些内容,让我来解释一下`assert_kind_of`。 这些称为断言。 你可以在此处查看哪些断言 [http://ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/Assertions.html#method-i-assert_respond_to](http://ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/Assertions.html#method-i-assert_respond_to) 。 断言是用于验证天气是否发生某些预期事件的函数,如果发生,则表示测试已通过,否则测试已失败。 现在让我们运行测试文件 test_chat_bot.rb ```rb $ ruby test_chat_bot.rb Run options: --seed 53866 # Running: E Finished in 0.000875s, 1142.2906 runs/s, 0.0000 assertions/s. 1) Error: TestChatBot#test_there_must_be_a_chat_bot: NameError: uninitialized constant TestChatBot::ChatBot test_chat_bot.rb:9:in `test_there_must_be_a_chat_bot' 1 runs, 0 assertions, 0 failures, 1 errors, 0 skips ``` 因此,我们得到上面的输出,表明不存在名为`ChatBot`的常量。 很好,我们尚未定义`ChatBot`是什么,因此我们将对其进行定义。 在 test_chatbot.rb 中,我们添加`require_relative "chat_bot.rb"`行,如下所示 ```rb # test_chat_bot.rb require "minitest/autorun" require_relative "chat_bot.rb" class TestChatBot < Minitest::Test # There must be a chat bot def test_there_must_be_a_chat_bot assert_kind_of ChatBot, ChatBot.new end # One must be able to set its age # One must be able to set its name # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" end ``` 然后,我们创建一个名为 chat_bot.rb 的新文件,其中包含以下内容 ```rb # chat_bot.rb class ChatBot end ``` 现在运行测试。 ```rb $ ruby test_chat_bot.rb Run options: --seed 19585 # Running: . Finished in 0.000720s, 1388.5244 runs/s, 1388.5244 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips ``` 我们做了什么? 我们有一个需求,我们编写了涵盖该需求的测试,我们运行了测试,但失败了,通过了,我们编写了足以使它通过的代码。 现在想象一个场景,你正在一个有 10 个开发人员的项目中,其中一个偶然地犯了一个错误,该错误将重命名此`ChatBot`类,并且你的测试将抓住它。 简而言之,如果你编写了足够的测试,则可以提早发现错误。 它不能保证没有错误的代码,但是会使错误弹出更加困难。 这些测试还将使你有信心重构代码。 假设你进行了更改,则不必担心你的更改可能会造成严重破坏,只需运行测试一次,你将获得有关失败和通过的报告。 让我们编写另一个测试,一个应该能够给聊天机器人一个`age`。 因此,让我们编写一个测试,在其中可以设置它的年龄并重新读取它。 查看下面的函数`test_one_must_be_able_to_set_its_age`中的代码 ```rb # test_chat_bot.rb require "minitest/autorun" require_relative "chat_bot.rb" class TestChatBot < Minitest::Test # There must be a chat bot def test_there_must_be_a_chat_bot assert_kind_of ChatBot, ChatBot.new end # One must be able to set its age def test_one_must_be_able_to_set_its_age age = 21 chat_bot = ChatBot.new chat_bot.age = age assert_equal age, chat_bot.age end # One must be able to set its name # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" end ``` 看上面的代码,看这行`assert_equal age, chat_bot.age`,在这里我们断言 chat_bot 返回设置的年龄。 ```rb $ ruby test_chat_bot.rb Run options: --seed 59168 # Running: .E Finished in 0.000855s, 2338.4784 runs/s, 1169.2392 assertions/s. 1) Error: TestChatBot#test_one_must_be_able_to_set_its_age: NoMethodError: undefined method `age=' for #<ChatBot:0x0000558b89da5380> test_chat_bot.rb:16:in `test_one_must_be_able_to_set_its_age' 2 runs, 1 assertions, 0 failures, 1 errors, 0 skips ``` 如果你看到上面的测试结果,则说明没有方法错误,并说明缺少函数`age=`,因此请对其进行修复。 ```rb # chat_bot.rb class ChatBot attr_accessor :age end ``` 因此,我们在`attr_accessor :age`行上方添加了内容,然后运行测试并通过了如下所示的测试 ```rb $ ruby test_chat_bot.rb Run options: --seed 42767 # Running: .. Finished in 0.000774s, 2583.4820 runs/s, 2583.4820 assertions/s. 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips ``` 现在让我们在名称上也做同样的事情,在年龄上我将不做解释,在本书中也不再赘述。 让我说说最后的测试。 它必须打招呼。 为此,我们编写了一个测试,如下面的函数`test_greeting_message`所示: ```rb # test_chat_bot.rb require "minitest/autorun" require_relative "chat_bot.rb" class TestChatBot < Minitest::Test # There must be a chat bot def test_there_must_be_a_chat_bot assert_kind_of ChatBot, ChatBot.new end # One must be able to set its age def test_one_must_be_able_to_set_its_age age = 21 chat_bot = ChatBot.new chat_bot.age = age assert_equal age, chat_bot.age end # One must be able to set its name def test_one_must_be_able_to_set_its_name name = "Zigor" chat_bot = ChatBot.new chat_bot.name = name assert_equal name, chat_bot.name end # Its greeting must say "Hello I am <name> and my age is <age>. # Nice to meet you!" def test_greeting_message name = "Zigor" age = 21 expected_message = "Hello I am #{name} and my age is #{age}. Nice to meet you!" chat_bot = ChatBot.new chat_bot.name = name chat_bot.age = age assert_equal expected_message, chat_bot.greeting_message end end ``` 现在我们运行它,如你所见,它自然会失败,如下所示 ```rb $ ruby test_chat_bot.rb Run options: --seed 8752 # Running: .E.. Finished in 0.001075s, 3720.5045 runs/s, 2790.3784 assertions/s. 1) Error: TestChatBot#test_greeting_message: NoMethodError: undefined method `greeting_message' for #<ChatBot:0x000055b4ae5a8620 @name="Zigor", @age=21> test_chat_bot.rb:39:in `test_greeting_message' 4 runs, 3 assertions, 0 failures, 1 errors, 0 skips ``` 因此我们修改了文件 [chat_bot.rb](code/chat_bot.rb) ,如下所示 ```rb # chat_bot.rb class ChatBot attr_accessor :age, :name def greeting_message "Hello I am #{name} and my age is #{age}. Nice to meet you!" end end ``` 现在我们运行测试,它通过如下所示: ```rb $ ruby test_chat_bot.rb Run options: --seed 16324 # Running: .... Finished in 0.001149s, 3480.2007 runs/s, 3480.2007 assertions/s. 4 runs, 4 assertions, 0 failures, 0 errors, 0 skips ``` 因此,现在我们有了一个应用程序和一个安全网,因此它具有更多的错误证明。 # 设计模式 **需要设计模式** 与现在使用的软件相比,当软件开始时,它很小,计算机功率很低,并且只能用于非常艰巨的任务,所以人们对单个人或紧密联系的小组可以维护的小程序感到满意。 但是,随着计算机变得越来越强大,计算机变得越来越复杂,项目变得越来越庞大,代码的结构成为一个重要的问题。 那就是设计模式揭晓的时候。 阅读本书的大多数人可能是 Ruby 初学者或中级,但是你可能需要从事实际项目。 即使你为个人项目选择了 Ruby,也更好地组织代码,因此对设计模式的需求变得至关重要。