Elixir 1.7 - Doctests, patterns and with

सिद्धांत, पैटर्न और साथ




elixir

सिद्धांत, पैटर्न और साथ

यह अध्याय मिक्स और ओटीपी गाइड का हिस्सा है और यह इस गाइड में पिछले अध्यायों पर निर्भर करता है। अधिक जानकारी के लिए, परिचय मार्गदर्शिका पढ़ें या साइडबार में अध्याय सूचकांक देखें।

इस अध्याय में, हम उस कोड को लागू करेंगे जो पहले अध्याय में वर्णित कमांडों को पार्स करता है:

CREATE shopping
OK

PUT shopping milk 1
OK

PUT shopping eggs 3
OK

GET shopping milk
1
OK

DELETE shopping eggs
OK

पार्सिंग किए जाने के बाद, हम अपने सर्वर को पार्स कमांड को भेजने के लिए अपडेट करेंगे :kv एप्लिकेशन को हमने पहले बनाया था।

Doctests

भाषा के मुखपृष्ठ पर, हम उल्लेख करते हैं कि अमृत भाषा में प्रथम श्रेणी के नागरिक को दस्तावेज बनाता है। हमने इस गाइड में कई बार इस अवधारणा को खोजा है, यह mix help माध्यम से हो सकता है या IEx कंसोल में h Enum या अन्य मॉड्यूल टाइप करके।

इस खंड में, हम पार्सिंग कार्यक्षमता को कार्यान्वित करेंगे, इसे दस्तावेज़ित करेंगे और सुनिश्चित करेंगे कि हमारा दस्तावेज़ सिद्धांतों के साथ अद्यतित है। यह हमें सटीक कोड नमूनों के साथ दस्तावेज़ प्रदान करने में मदद करता है।

चलिए हमारी कमांड पार्सर को lib/kv_server/command.ex पर बनाते हैं और lib/kv_server/command.ex शुरू करते हैं:

defmodule KVServer.Command do
  @doc ~S"""
  Parses the given `line` into a command.

  ## Examples

      iex> KVServer.Command.parse("CREATE shopping\r\n")
      {:ok, {:create, "shopping"}}

  """
  def parse(_line) do
    :not_implemented
  end
end

सिद्धांत चार स्थानों के इंडेंट द्वारा निर्दिष्ट किए गए हैं, इसके बाद प्रलेखन स्ट्रिंग में iex> प्रॉम्प्ट द्वारा पीछा किया जाता है। यदि कोई कमांड कई लाइनों को फैलाती है, तो आप ...> उपयोग कर सकते हैं, जैसा कि IEx में है। अपेक्षित परिणाम iex> या ...> लाइन (ओं) के बाद अगली पंक्ति में शुरू होना चाहिए और या तो एक newline या एक नया iex> उपसर्ग द्वारा समाप्त किया जाता है।

इसके अलावा, ध्यान दें कि हमने @doc ~S""" का उपयोग करके दस्तावेज़ीकरण स्ट्रिंग शुरू कर दिया है। ~S \r\n वर्णों को गाड़ी के रिटर्न और लाइन फीड में परिवर्तित होने से रोकता है जब तक कि उनका परीक्षण में मूल्यांकन नहीं किया जाता है।

अपने सिद्धांतों को चलाने के लिए, हम test/kv_server/command_test.exs पर एक फ़ाइल test/kv_server/command_test.exs और परीक्षण मामले में doctest KVServer.Command कॉल doctest KVServer.Command :

defmodule KVServer.CommandTest do
  use ExUnit.Case, async: true
  doctest KVServer.Command
end

परीक्षण सूट चलाएं और सिद्धांत विफल होना चाहिए:

  1) test doc at KVServer.Command.parse/1 (1) (KVServer.CommandTest)
     test/kv_server/command_test.exs:3
     Doctest failed
     code: KVServer.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
     lhs:  :not_implemented
     stacktrace:
       lib/kv_server/command.ex:7: KVServer.Command (module)

अति उत्कृष्ट!

अब आइए doctest पास बनाते हैं। आइए लागू करें parse/1 फ़ंक्शन:

def parse(line) do
  case String.split(line) do
    ["CREATE", bucket] -> {:ok, {:create, bucket}}
  end
end

हमारा कार्यान्वयन व्हॉट्सएप पर लाइन को विभाजित करता है और फिर एक सूची के खिलाफ कमांड से मेल खाता है। String.split/1 का उपयोग करने का अर्थ है कि हमारे आदेश व्हाट्सएप-असंवेदनशील होंगे। व्हाट्सएप का प्रमुख और अनुगामी होना मायने नहीं रखेगा, और न ही शब्दों के बीच लगातार रिक्त स्थान होगा। आइए अन्य आदेशों के साथ इस व्यवहार का परीक्षण करने के लिए कुछ नए सिद्धांत जोड़ें:

@doc ~S"""
Parses the given `line` into a command.

## Examples

    iex> KVServer.Command.parse "CREATE shopping\r\n"
    {:ok, {:create, "shopping"}}

    iex> KVServer.Command.parse "CREATE  shopping  \r\n"
    {:ok, {:create, "shopping"}}

    iex> KVServer.Command.parse "PUT shopping milk 1\r\n"
    {:ok, {:put, "shopping", "milk", "1"}}

    iex> KVServer.Command.parse "GET shopping milk\r\n"
    {:ok, {:get, "shopping", "milk"}}

    iex> KVServer.Command.parse "DELETE shopping eggs\r\n"
    {:ok, {:delete, "shopping", "eggs"}}

Unknown commands or commands with the wrong number of
arguments return an error:

    iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
    {:error, :unknown_command}

    iex> KVServer.Command.parse "GET shopping\r\n"
    {:error, :unknown_command}

"""

हाथ में सिद्धांत के साथ, परीक्षणों को पास करने की आपकी बारी है! एक बार तैयार होने के बाद, आप नीचे दिए गए समाधान के साथ अपने काम की तुलना कर सकते हैं:

def parse(line) do
  case String.split(line) do
    ["CREATE", bucket] -> {:ok, {:create, bucket}}
    ["GET", bucket, key] -> {:ok, {:get, bucket, key}}
    ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
    ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
    _ -> {:error, :unknown_command}
  end
end

ध्यान दें कि कैसे हम आदेशों का तर्क और संख्याओं की जांच करने वाले खंडों को जोड़ने के बिना आदेशों को शिथिल रूप से पार्स करने में सक्षम थे!

अंत में, आपने देखा होगा कि प्रत्येक सिद्धांत हमारे सुइट में एक अलग परीक्षण से मेल खाता है, जो अब कुल 7 सिद्धांतों की रिपोर्ट करता है। ऐसा इसलिए है क्योंकि एक्सयूनाइट दो अलग-अलग सिद्धांतों को परिभाषित करने के लिए निम्नलिखित पर विचार करता है:

iex> KVServer.Command.parse("UNKNOWN shopping eggs\r\n")
{:error, :unknown_command}

iex> KVServer.Command.parse("GET shopping\r\n")
{:error, :unknown_command}

नई लाइनों के बिना, जैसा कि नीचे देखा गया है, एक्सयूनाइट इसे एक ही सिद्धांत में संकलित करता है:

iex> KVServer.Command.parse("UNKNOWN shopping eggs\r\n")
{:error, :unknown_command}
iex> KVServer.Command.parse("GET shopping\r\n")
{:error, :unknown_command}

जैसा कि नाम कहता है, सिद्धांत पहले प्रलेखन है और बाद में एक परीक्षण है। उनका लक्ष्य परीक्षणों को प्रतिस्थापित करना नहीं है, बल्कि तिथि प्रलेखन तक प्रदान करना है। आप ExUnit.DocTest डॉक्स में सिद्धांतों के बारे में अधिक पढ़ सकते हैं।

साथ में

जैसा कि हम अब आदेशों को पार्स करने में सक्षम हैं, हम अंततः आदेशों को चलाने वाले तर्क को लागू करना शुरू कर सकते हैं। आइए अब इस फ़ंक्शन के लिए एक स्टब परिभाषा जोड़ें:

defmodule KVServer.Command do
  @doc """
  Runs the given command.
  """
  def run(command) do
    {:ok, "OK\r\n"}
  end
end

इससे पहले कि हम इस फ़ंक्शन को लागू करें, अपने नए parse/1 और run/1 फ़ंक्शन का उपयोग शुरू करने के लिए अपना सर्वर बदलें। याद रखें, जब क्लाइंट ने सॉकेट बंद किया था, तो हमारा read_line/1 फ़ंक्शन भी क्रैश हो रहा था, इसलिए चलो इसे ठीक करने का अवसर भी लें। lib/kv_server.ex खोलें और मौजूदा सर्वर परिभाषा को बदलें:

defp serve(socket) do
  socket
  |> read_line()
  |> write_line(socket)

  serve(socket)
end

defp read_line(socket) do
  {:ok, data} = :gen_tcp.recv(socket, 0)
  data
end

defp write_line(line, socket) do
  :gen_tcp.send(socket, line)
end

निम्नलिखित द्वारा:

defp serve(socket) do
  msg =
    case read_line(socket) do
      {:ok, data} ->
        case KVServer.Command.parse(data) do
          {:ok, command} ->
            KVServer.Command.run(command)
          {:error, _} = err ->
            err
        end
      {:error, _} = err ->
        err
    end

  write_line(socket, msg)
  serve(socket)
end

defp read_line(socket) do
  :gen_tcp.recv(socket, 0)
end

defp write_line(socket, {:ok, text}) do
  :gen_tcp.send(socket, text)
end

defp write_line(socket, {:error, :unknown_command}) do
  # Known error. Write to the client.
  :gen_tcp.send(socket, "UNKNOWN COMMAND\r\n")
end

defp write_line(_socket, {:error, :closed}) do
  # The connection was closed, exit politely.
  exit(:shutdown)
end

defp write_line(socket, {:error, error}) do
  # Unknown error. Write to the client and exit.
  :gen_tcp.send(socket, "ERROR\r\n")
  exit(error)
end

यदि हम अपना सर्वर शुरू करते हैं, तो हम अब इसे कमांड भेज सकते हैं। अभी के लिए, हमें दो अलग-अलग प्रतिक्रियाएँ मिलेंगी: "ठीक है" जब कमांड ज्ञात हो और "UNKNOWN COMMAND" अन्यथा:

$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
CREATE shopping
OK
HELLO
UNKNOWN COMMAND

इसका मतलब यह है कि हमारा कार्यान्वयन सही दिशा में जा रहा है, लेकिन यह बहुत सुंदर नहीं दिखता है, क्या यह है?

पिछले कार्यान्वयन में पाइपलाइनों का उपयोग किया गया था जो तर्क का पालन करने के लिए सीधा था। हालाँकि, अब हमें अलग-अलग त्रुटि कोड्स को संभालने की आवश्यकता है, हमारे सर्वर लॉजिक को कई case कॉल के अंदर नेस्टेड किया जाता है।

शुक्र है, एलिक्सिर v1.2 ने निर्माण के with पेश किया, जो आपको उपरोक्त कोड की तरह कोड को सरल बनाने की अनुमति देता है, नेस्टेड case कॉल की जगह मेलिंग क्लॉस की एक श्रृंखला के साथ। चलो serve/1 फ़ंक्शन को फिर से लिखने के लिए उपयोग with :

defp serve(socket) do
  msg =
    with {:ok, data} <- read_line(socket),
         {:ok, command} <- KVServer.Command.parse(data),
         do: KVServer.Command.run(command)

  write_line(socket, msg)
  serve(socket)
end

काफी बेहतर! with <- के दाईं ओर लौटाए गए मान को पुनः प्राप्त करेगा <- और बाईं ओर के पैटर्न के खिलाफ इसे मैच करेगा। यदि मान पैटर्न से मेल खाता है, तो अगली अभिव्यक्ति पर आगे बढ़ता है। यदि कोई मिलान नहीं है, तो गैर-मिलान मूल्य वापस आ जाता है।

दूसरे शब्दों में, हमने case/2 को दिए गए प्रत्येक एक्सप्रेशन को कदम के रूप में परिवर्तित with । जैसे ही कोई भी चरण कुछ ऐसा लौटाता है जो निरस्त होने के साथ {:ok, x} मेल नहीं खाता है, और गैर-मिलान मूल्य लौटाता है।

आप हमारे दस्तावेज़ीकरण के बारे में अधिक पढ़ सकते हैं।

आज्ञा चलाना

अंतिम चरण KVServer.Command.run/1 को लागू करने के लिए है: KVServer.Command.run/1 अनुप्रयोग के विरुद्ध पार्स कमांड को चलाने के लिए। इसका कार्यान्वयन नीचे दिखाया गया है:

@doc """
Runs the given command.
"""
def run(command)

def run({:create, bucket}) do
  KV.Registry.create(KV.Registry, bucket)
  {:ok, "OK\r\n"}
end

def run({:get, bucket, key}) do
  lookup(bucket, fn pid ->
    value = KV.Bucket.get(pid, key)
    {:ok, "#{value}\r\nOK\r\n"}
  end)
end

def run({:put, bucket, key, value}) do
  lookup(bucket, fn pid ->
    KV.Bucket.put(pid, key, value)
    {:ok, "OK\r\n"}
  end)
end

def run({:delete, bucket, key}) do
  lookup(bucket, fn pid ->
    KV.Bucket.delete(pid, key)
    {:ok, "OK\r\n"}
  end)
end

defp lookup(bucket, callback) do
  case KV.Registry.lookup(KV.Registry, bucket) do
    {:ok, pid} -> callback.(pid)
    :error -> {:error, :not_found}
  end
end

प्रत्येक फ़ंक्शन क्लॉज KV.Registry सर्वर को उपयुक्त कमांड KV.Registry है जिसे हमने :kv एप्लिकेशन स्टार्टअप के दौरान पंजीकृत किया था। चूंकि हमारा :kv_server इस पर निर्भर करता है :kv एप्लिकेशन, यह उन सेवाओं पर निर्भर करने के लिए पूरी तरह से ठीक है जो इसे प्रदान करती है।

आपने देखा होगा कि हमारे पास एक फंक्शन हेड है, def run(command) , एक बॉडी के बिना। मॉड्यूल और फ़ंक्शंस अध्याय में, हमने सीखा कि एक बहु-फ़ंक्शन फ़ंक्शन के लिए डिफ़ॉल्ट तर्क घोषित करने के लिए एक बॉडीलेस फ़ंक्शन का उपयोग किया जा सकता है। यहाँ एक और उपयोग का मामला है जहाँ हम एक फ़ंक्शन का उपयोग करते हैं एक निकाय के बिना यह दस्तावेज़ करने के लिए कि तर्क क्या हैं

ध्यान दें कि हमने एक निजी फ़ंक्शन को lookup/2 नामक एक निजी फ़ंक्शन को परिभाषित किया है ताकि एक बाल्टी को lookup/2 की सामान्य कार्यक्षमता में मदद मिल सके और अगर यह मौजूद है, तो यह वापस आ जाएगी, {:error, :not_found} अन्यथा।

वैसे, चूंकि हम अब लौट रहे हैं {:error, :not_found} , हमें KVServer में KVServer write_line/2 फ़ंक्शन में संशोधन करना चाहिए ताकि इस तरह की त्रुटि को भी प्रिंट किया जा KVServer :

defp write_line(socket, {:error, :not_found}) do
  :gen_tcp.send(socket, "NOT FOUND\r\n")
end

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

KVServer.Command.run/1 का कार्यान्वयन KV.Registry नामक सर्वर पर सीधे कमांड भेज रहा है, जो कि :kv एप्लिकेशन द्वारा पंजीकृत है। इसका मतलब यह है कि यह सर्वर वैश्विक है और अगर हमारे पास एक ही समय में संदेश भेजने वाले दो परीक्षण हैं, तो हमारे परीक्षण एक-दूसरे के साथ संघर्ष करेंगे (और संभावित रूप से विफल)। हमें इकाई परीक्षणों के बीच निर्णय लेने की आवश्यकता है जो अलग-थलग हैं और अतुल्यकालिक रूप से चल सकते हैं, या एकीकरण परीक्षण लिख सकते हैं जो वैश्विक स्थिति के शीर्ष पर काम करते हैं, लेकिन हमारे आवेदन के पूर्ण स्टैक का उपयोग करते हैं क्योंकि यह उत्पादन में प्रयोग किया जाना है।

अब तक हमारे पास केवल लिखित इकाई परीक्षण हैं, आम तौर पर सीधे एकल मॉड्यूल का परीक्षण। हालांकि, KVServer.Command.run/1 को एक इकाई के रूप में परीक्षण योग्य बनाने के लिए, हमें इसके कार्यान्वयन को बदलने की आवश्यकता होगी ताकि आदेश सीधे KV.Registry प्रक्रिया में न भेजें लेकिन इसके बजाय एक सर्वर को एक तर्क के रूप में पास करें। उदाहरण के लिए, हमें def run(command, pid) लिए हस्ताक्षर को बदलने की आवश्यकता होगी और फिर उसके अनुसार सभी खंडों को बदलना होगा:

def run({:create, bucket}, pid) do
  KV.Registry.create(pid, bucket)
  {:ok, "OK\r\n"}
end

# ... other run clauses ...

बेझिझक आगे बढ़ें और ऊपर किए गए बदलावों को लिखें और कुछ यूनिट टेस्ट लिखें। विचार यह है कि आपके परीक्षण KV.Registry का एक उदाहरण शुरू करेंगे और इसे वैश्विक KV.Registry पर भरोसा करने के बजाय run/2 तर्क के रूप में पास करेंगे। यह हमारे परीक्षणों को अतुल्यकालिक रखने का लाभ है क्योंकि कोई साझा स्थिति नहीं है।

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

हमारा एकीकरण परीक्षण एक टीसीपी क्लाइंट का उपयोग करेगा जो हमारे सर्वर को कमांड भेजता है और दावा करता है कि हमें वांछित प्रतिक्रियाएं मिल रही हैं।

चलो नीचे दिए गए अनुसार test/kv_server_test.exs में एकीकरण परीक्षण लागू करें:

defmodule KVServerTest do
  use ExUnit.Case

  setup do
    Application.stop(:kv)
    :ok = Application.start(:kv)
  end

  setup do
    opts = [:binary, packet: :line, active: false]
    {:ok, socket} = :gen_tcp.connect('localhost', 4040, opts)
    %{socket: socket}
  end

  test "server interaction", %{socket: socket} do
    assert send_and_recv(socket, "UNKNOWN shopping\r\n") ==
           "UNKNOWN COMMAND\r\n"

    assert send_and_recv(socket, "GET shopping eggs\r\n") ==
           "NOT FOUND\r\n"

    assert send_and_recv(socket, "CREATE shopping\r\n") ==
           "OK\r\n"

    assert send_and_recv(socket, "PUT shopping eggs 3\r\n") ==
           "OK\r\n"

    # GET returns two lines
    assert send_and_recv(socket, "GET shopping eggs\r\n") == "3\r\n"
    assert send_and_recv(socket, "") == "OK\r\n"

    assert send_and_recv(socket, "DELETE shopping eggs\r\n") ==
           "OK\r\n"

    # GET returns two lines
    assert send_and_recv(socket, "GET shopping eggs\r\n") == "\r\n"
    assert send_and_recv(socket, "") == "OK\r\n"
  end

  defp send_and_recv(socket, command) do
    :ok = :gen_tcp.send(socket, command)
    {:ok, data} = :gen_tcp.recv(socket, 0, 1000)
    data
  end
end

हमारा एकीकरण परीक्षण सभी सर्वर इंटरैक्शन की जांच करता है, जिसमें अज्ञात कमांड शामिल हैं और त्रुटियां नहीं मिली हैं। यह ध्यान देने योग्य है कि, ईटीएस तालिकाओं और लिंक की गई प्रक्रियाओं के साथ, सॉकेट को बंद करने की आवश्यकता नहीं है। एक बार परीक्षण प्रक्रिया से बाहर निकलने के बाद, सॉकेट अपने आप बंद हो जाता है।

इस बार, चूंकि हमारा परीक्षण वैश्विक डेटा पर निर्भर करता है, इसलिए हमने use ExUnit.Case नहीं दिया async: true use ExUnit.Case का use ExUnit.Case । इसके अलावा, हमारे परीक्षण की गारंटी देने के लिए हम हमेशा साफ स्थिति में रहते हैं, हम रोकते हैं और शुरू करते हैं :kv प्रत्येक परीक्षण से पहले :kv आवेदन। वास्तव में, :kv एप्लिकेशन को रोकना भी टर्मिनल पर एक चेतावनी प्रिंट करता है:

18:12:10.698 [info] Application kv exited: :stopped

परीक्षणों के दौरान लॉग संदेशों को प्रिंट करने से बचने के लिए, ExUnit एक स्वच्छ सुविधा प्रदान करता है जिसे :capture_log कैप्चर_लॉग कहते हैं। प्रत्येक परीक्षण से पहले @tag :capture_log सेट करके या @moduletag :capture_log पूरे परीक्षण मामले के लिए, ExUnit स्वचालित रूप से परीक्षण रन करते समय लॉग की गई किसी भी चीज़ को कैप्चर करेगा। यदि हमारा परीक्षण विफल हो जाता है, तो कैप्चर किए गए लॉग को एक्सनिट रिपोर्ट के साथ मुद्रित किया जाएगा।

use ExUnit.Case और सेटअप के बीच, निम्न कॉल जोड़ें:

@moduletag :capture_log

परीक्षण क्रैश होने की स्थिति में, आप एक रिपोर्ट देखेंगे:

  1) test server interaction (KVServerTest)
     test/kv_server_test.exs:17
     ** (RuntimeError) oops
     stacktrace:
       test/kv_server_test.exs:29

     The following output was logged:

     13:44:10.035 [info]  Application kv exited: :stopped

इस सरल एकीकरण परीक्षण के साथ, हम यह देखना शुरू करते हैं कि एकीकरण परीक्षण धीमा क्यों हो सकता है। इतना ही नहीं यह परीक्षण अतुल्यकालिक रूप से नहीं चल सकता है, इसके लिए :kv एप्लिकेशन को रोकने और शुरू करने के महंगे सेटअप की आवश्यकता होती है। वास्तव में, आपका परीक्षण सूट भी विफल हो सकता है और टाइमआउट में चल सकता है। अगर ऐसा है, तो आप एक तीसरे तर्क को पारित करने के लिए :gen_tcp.recv(socket, 0) कॉल को ट्वीक कर सकते हैं, जो कि मिलीसेकंड में टाइमआउट है। अगले अध्याय में हम एप्लिकेशन कॉन्फ़िगरेशन के बारे में जानेंगे, जिसका उपयोग हम समय-समय पर विन्यास योग्य बनाने के लिए कर सकते हैं, यदि वांछित है।

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

चलिए आखिरी अध्याय पर चलते हैं। हम अंत में एक बकेट रूटिंग तंत्र को जोड़कर हमारे सिस्टम को वितरित करेंगे। हम एप्लिकेशन कॉन्फ़िगरेशन के बारे में भी जानेंगे।