Notes

はじめに

  • これまで、HTMLとCSSを使って簡単なWebページを構築する方法、GitとGitHubを使ってコードの変更を追跡し、他の人と共同作業するための方法を説明してきました。また、Pythonプログラミング言語に慣れ、Djangoを使用してWebアプリケーションを作成し始め、Djangoモデルを使用してサイトに情報を保存する方法を学びました。その後、JavaScriptを導入してWebページをよりインタラクティブにする方法を学習し、アニメーションとReactを使用してユーザインターフェイスをさらに改善する方法について説明しました。
  • 今日は、より大規模なプロジェクトに取り組む場合のベスト・プラクティスについて説明します。

テスト

ソフトウェア開発プロセスの重要な部分の1つは、作成したコードをテストし、すべてが期待通りに動作することを確認することです。このレッスンでは、コードのテスト方法を改善するいくつかの手法について説明します。

Assert

Pythonでテストを実行する最も簡単な方法の1つは、assert コマンドを使用することです。このコマンドの後には、True となるはずの式が続きます。式が True の場合は何も発生せず、False の場合は例外がスローされます。最初にPythonを学んだときに書いた square 関数をテストするために、このコマンドを組み込む方法を見てみましょう。関数が正しく実行され、assert が True の場合は何も起こりません。

def square(x):
   return x * x

assert square(10) == 100

""" Output:

"""

ですが、結果が間違っている場合は、例外が出力されます。

def square(x):
   return x + x

assert square(10) == 100

""" Output:
Traceback (most recent call last):
  File "assert.py", line 4, in <module>
    assert square(10) == 100
AssertionError
"""

Test-Driven Development (テスト駆動開発)

大規模なプロジェクトの構築を開始する際には、テスト駆動開発(test-driven developmentの使用を検討することをお勧めします。テスト駆動開発とは、バグを修正するたびに、そのバグをチェックするテストを、変更を加えるたびに実行される一連のテストに追加する開発スタイルです。これにより、プロジェクトの追加機能が既存の機能と干渉しないようにすることができます。

次に、もう少し複雑な関数を見て、テストを作成することでエラーを見つける方法を考えてみましょう。次に、入力が素数の場合にのみ True を返す is_prime という関数を作成します。

 import math

def is_prime(n):

    # We know numbers less than 2 are not prime
    if n < 2:
        return False

    # Checking factors up to sqrt(n)
    for i in range(2, int(math.sqrt(n))):

        # If i is a factor, return false
        if n % i == 0:
            return False

    # If no factors were found, return true
    return True

では、prime 関数をテストするために作成した関数を見てみましょう。

 from prime import is_prime

def test_prime(n, expected):
    if is_prime(n) != expected:
        print(f"ERROR on is_prime({n}), expected {expected}")

この時点で、Pythonインタプリタに入り、いくつかの値をテストすることができます。

>>> test_prime(5, True)
>>> test_prime(10, False)
>>> test_prime(25, False)
ERROR on is_prime(25), expected False

上記の出力から、5と10は素数として正しく識別され、素数ではないことがわかりますが、25は誤って素数として識別されているため、この関数には何か問題があるに違いありません。この機能の問題点を調べる前に、テストを自動化する方法について説明します。これを行う1つの方法は、シェルスクリプトを作成するか、ターミナル上で実行できるスクリプトを作成することです。これらのファイルには .sh 拡張子が必要なので、ファイル名は tests0.sh になります。次の各行は、次の要素で構成されています。

  1. python3 で実行しているPythonのバージョンを指定する
  2. -c でコマンドを実行することを示す
  3. 文字列形式で実行するコマンド
python3 -c "from tests0 import test_prime; test_prime(1, False)"
python3 -c "from tests0 import test_prime; test_prime(2, True)"
python3 -c "from tests0 import test_prime; test_prime(8, False)"
python3 -c "from tests0 import test_prime; test_prime(11, True)"
python3 -c "from tests0 import test_prime; test_prime(25, False)"
python3 -c "from tests0 import test_prime; test_prime(28, False)"

これで ./tests0.sh をターミナル上で実行してこれらのコマンドを実行できるようになりました。

ERROR on is_prime(8), expected False
ERROR on is_prime(25), expected False

ユニットテスト

上記の方法を使用してテストを自動的に実行することができたとしても、これらのテストをそれぞれ書き出す必要はありません。ありがたいことに、このプロセスを少し簡単にするためにPython unittest ライブラリを使うことができます。テストプログラムが is_prime 関数に対してどのように見えるかを見てみましょう。

# Import the unittest library and our function
import unittest
from prime import is_prime

# A class containing all of our tests
class Tests(unittest.TestCase):

    def test_1(self):
        """Check that 1 is not prime."""
        self.assertFalse(is_prime(1))

    def test_2(self):
        """Check that 2 is prime."""
        self.assertTrue(is_prime(2))

    def test_8(self):
        """Check that 8 is not prime."""
        self.assertFalse(is_prime(8))

    def test_11(self):
        """Check that 11 is prime."""
        self.assertTrue(is_prime(11))

    def test_25(self):
        """Check that 25 is not prime."""
        self.assertFalse(is_prime(25))

    def test_28(self):
        """Check that 28 is not prime."""
        self.assertFalse(is_prime(28))


# Run each of the testing functions
if __name__ == "__main__":
    unittest.main()

Tests クラス内の各関数がパターンに従っていることに注意してください。

  • 関数の名前は test_ で始まります。これは、unittest.main() の呼び出しで関数が自動的に実行されるために必要です。
  • 各テストは self 引数を取ります。これは、Pythonクラス内でメソッドを作成する場合の標準です。
  • 各関数の最初の行には、3つの引用符で囲まれたdocstringが含まれます。これらは単にコードを読みやすくするためだけのものではありません。テストが実行されると、テストが失敗した場合にコメントがテストの説明として表示されます。
  • 各関数の次の行には、self.assertSOMETHING という形式のアサーションが含まれていました。assertTrueassertFalseassertEqualassertGreater など、さまざまなアサーションを作成できます。詳細については、こちらのドキュメントを参照してください。

次に、これらのテストの結果を確認します。

...F.F
======================================================================
FAIL: test_25 (__main__.Tests)
Check that 25 is not prime.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests1.py", line 26, in test_25
    self.assertFalse(is_prime(25))
AssertionError: True is not false

======================================================================
FAIL: test_8 (__main__.Tests)
Check that 8 is not prime.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests1.py", line 18, in test_8
    self.assertFalse(is_prime(8))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (failures=2)

テストの実行後、unittest は、検出された内容に関する有用な情報を提供します。最初の行では、テストが記述された順に、成功の場合は一連の . 、失敗の場合は F が表示されます。

...F.F

次に、失敗した各テストに対して、失敗した関数の名前が与えられます。

FAIL: test_25 (__main__.Tests)

先ほど説明したコメント:

Check that 25 is not prime.

例外のトレースバック:

Traceback (most recent call last):
  File "tests1.py", line 26, in test_25
    self.assertFalse(is_prime(25))
AssertionError: True is not false

最後に、実行されたテストの数、実行にかかった時間、失敗したテストの数を確認します。

Ran 6 tests in 0.001s

FAILED (failures=2)

では、関数のバグを修正する方法を見てみましょう。結果として、for ループでもう1つの番号をテストする必要があります。たとえば、n25 の場合、平方根は 5 になりますが、これが range 関数の引数の一つである場合、for ループは数値 4 で終了します。したがって、for ループのヘッダーを単純に次のように変更できます。

for i in range(2, int(math.sqrt(n)) + 1):

ここで、単体テストを使用してテストを再度実行すると、変更によってバグが修正されたことを示す次の出力が得られます。

......
----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK

これらの自動化されたテストは、この機能を最適化すると、さらに便利になります。たとえば、すべての整数を係数としてチェックする必要がなく、より小さな素数 (数が3で割り切れない場合は、6、9、12、…で割り切れません) だけをチェックするという事実を使用したり、FermatMiller-Rabinの素数判定など、より高度な確率的素数判定テストを使用したりできます。この機能を改善するために変更を加えるたびに、ユニットテストを簡単に再実行して、機能が正しいことを確認する機能が必要になります。

Djangoテスト

では、Djangoアプリケーションを作成する際に自動テストの概念をどのように適用できるかを見てみましょう。この作業では、最初にDjangoモデルについて学んだときに作成した flights プロジェクトを使用します。最初に、2つの条件をチェックしてフライトが有効であることを確認するメソッドを Flight モデルに追加します。

  1. 出発点が目的地と同じではありません
  2. 所要時間が0分を超えています

モデルは次のようになります。

class Flight(models.Model):
    origin = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="departures")
    destination = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="arrivals")
    duration = models.IntegerField()

    def __str__(self):
        return f"{self.id}: {self.origin} to {self.destination}"

    def is_valid_flight(self):
        return self.origin != self.destination or self.duration > 0

アプリケーションが期待どおりに動作することを確認するために、新しいアプリケーションを作成するたびに、tests.py ファイルが自動的に作成されます。このファイルを最初に開くと、DjangoのTestCaseライブラリが自動的にインポートされていることが分かります。

from django.test import TestCase

TestCase ライブラリーを使用する利点の1つは、テストを実行すると、テスト専用のまったく新しいデータベースが作成されることです。これにより、データベース内の既存のエントリを誤って変更したり削除したりするリスクを回避でき、テスト用に作成したダミーエントリを削除する心配がなくなります。

このライブラリを使用するには、まずすべてのモデルをインポートします。

from .models import Flight, Airport, Passenger

次に、インポートした TestCase クラスを拡張する新しいクラスを作成します。このクラスでは、テスト・プロセスの開始時に実行される setUp 関数を定義します。この関数の中に私たちがテストしたい内容を含めていきます。まず、クラスがどのようになるかを説明します。

class FlightTestCase(TestCase):

    def setUp(self):

        # Create airports.
        a1 = Airport.objects.create(code="AAA", city="City A")
        a2 = Airport.objects.create(code="BBB", city="City B")

        # Create flights.
        Flight.objects.create(origin=a1, destination=a2, duration=100)
        Flight.objects.create(origin=a1, destination=a1, duration=200)
        Flight.objects.create(origin=a1, destination=a2, duration=-100)

テスト用データベースにいくつかのエントリーがあるので、このクラスにいくつかのテストを実行する関数を追加します。まず、AAA 空港からの出発(3になるはずです)と到着(1になるはずです)の数をカウントすることで、出発 departures と到着 arrivals のフィールドが正しく機能することを確認しましょう。

def test_departures_count(self):
    a = Airport.objects.get(code="AAA")
    self.assertEqual(a.departures.count(), 3)

def test_arrivals_count(self):
    a = Airport.objects.get(code="AAA")
    self.assertEqual(a.arrivals.count(), 1)

Flight モデルに追加した is_valid_flight 関数もテストできます。まず、Flight が有効な場合に関数がtrueを返すことを確認します。

def test_valid_flight(self):
    a1 = Airport.objects.get(code="AAA")
    a2 = Airport.objects.get(code="BBB")
    f = Flight.objects.get(origin=a1, destination=a2, duration=100)
    self.assertTrue(f.is_valid_flight())

次に、無効な目的地および期間を含むフライトがfalseを返すことを確認します。

def test_invalid_flight_destination(self):
    a1 = Airport.objects.get(code="AAA")
    f = Flight.objects.get(origin=a1, destination=a1)
    self.assertFalse(f.is_valid_flight())

def test_invalid_flight_duration(self):
    a1 = Airport.objects.get(code="AAA")
    a2 = Airport.objects.get(code="BBB")
    f = Flight.objects.get(origin=a1, destination=a2, duration=-100)
    self.assertFalse(f.is_valid_flight())

次に、テストを実行するために、python manage.py test を実行します。この出力は、Python unittest ライブラリを使用した場合の出力とほとんど同じですが、テスト用データベースの作成と破棄のログも記録されます。

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..FF.
======================================================================
FAIL: test_invalid_flight_destination (flights.tests.FlightTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/cleggett/Documents/cs50/web_notes_files/7/django/airline/flights/tests.py", line 37, in test_invalid_flight_destination
    self.assertFalse(f.is_valid_flight())
AssertionError: True is not false

======================================================================
FAIL: test_invalid_flight_duration (flights.tests.FlightTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/cleggett/Documents/cs50/web_notes_files/7/django/airline/flights/tests.py", line 43, in test_invalid_flight_duration
    self.assertFalse(f.is_valid_flight())
AssertionError: True is not false

----------------------------------------------------------------------
Ran 5 tests in 0.018s

FAILED (failures=2)
Destroying test database for alias 'default'...

上記の出力から、is_valid_flightFalse を返すべきところに True を返す場合があることがわかります。この関数をさらに調べてみると、私たちが and の代わりに or を使用したというミスを犯したことがわかります。これは、flight が有効になるためには、フライト要件の1つだけを満たさなければならないことを意味します。関数を次のように変更します。

 def is_valid_flight(self):
    return self.origin != self.destination and self.duration > 0

次に、より良い結果を得るために再度テストを実行します。

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.014s

OK
Destroying test database for alias 'default'...

クライアントのテスト

Webアプリケーションを作成するときには、特定の機能が動作するかどうかだけでなく、個々のWebページが意図したとおりにロードされるかどうかも確認する必要があります。そのためには、Djangoテスト・クラスで Client オブジェクトを作成し、そのオブジェクトを使用して要求を作成します。これを行うには、まずインポートに Client を追加する必要があります。

 from django.test import Client, TestCase

たとえば、HTTPレスポンスコード200を取得し、フライトがすべてレスポンスのコンテキストに追加されることを確認するテストを追加します。

def test_index(self):

    # Set up client to make requests
    c = Client()

    # Send get request to index page and store response
    response = c.get("/flights/")

    # Make sure status code is 200
    self.assertEqual(response.status_code, 200)

    # Make sure three flights are returned in the context
    self.assertEqual(response.context["flights"].count(), 3)

同様に、有効なフライトページの有効な応答コードと、存在しないフライトページの無効な応答コードを取得できるかどうかを確認できます。( Max 関数を使用して、django.db.models import Max をファイルの先頭に含めることでアクセスできる最大 id を見つけます。)

def test_valid_flight_page(self):  
    a1 = Airport.objects.get(code="AAA")
    f = Flight.objects.get(origin=a1, destination=a1)

    c = Client()
    response = c.get(f"/flights/{f.id}")
    self.assertEqual(response.status_code, 200)

def test_invalid_flight_page(self):
    max_id = Flight.objects.all().aggregate(Max("id"))["id__max"]

    c = Client()
    response = c.get(f"/flights/{max_id + 1}")
    self.assertEqual(response.status_code, 404)

最後に、乗客リストと非乗客リストが予想どおりに生成されていることを確認するためのテストをいくつか追加します。

def test_flight_page_passengers(self):
    f = Flight.objects.get(pk=1)
    p = Passenger.objects.create(first="Alice", last="Adams")
    f.passengers.add(p)

    c = Client()
    response = c.get(f"/flights/{f.id}")
    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.context["passengers"].count(), 1)

def test_flight_page_non_passengers(self):
    f = Flight.objects.get(pk=1)
    p = Passenger.objects.create(first="Alice", last="Adams")

    c = Client()
    response = c.get(f"/flights/{f.id}")
    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.context["non_passengers"].count(), 1)

これで、すべてのテストを一緒に実行して、現時点でエラーがないことを確認できます。

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.048s

OK
Destroying test database for alias 'default'...

Selenium

これまでのところ、PythonとDjangoを使って作成したサーバ側のコードをテストすることはできましたが、アプリケーションを構築する際には、クライアント側のコードのテストも作成できるようにしたいと考えています。たとえば、counter.html ページに戻ってテストを作成してみましょう。

まず、少し異なるカウンタページを作成し、カウントを減らすボタンを追加します。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Counter</title>
        <script>
            
            // Wait for page to load
            document.addEventListener('DOMContentLoaded', () => {

                // Initialize variable to 0
                let counter = 0;

                // If increase button clicked, increase counter and change inner html
                document.querySelector('#increase').onclick = () => {
                    counter ++;
                    document.querySelector('h1').innerHTML = counter;
                }

                // If decrease button clicked, decrease counter and change inner html
                document.querySelector('#decrease').onclick = () => {
                    counter --;
                    document.querySelector('h1').innerHTML = counter;
                }
            })
        </script>
    </head>
    <body>
        <h1>0</h1>
        <button id="increase">+</button>
        <button id="decrease">-</button>
    </body>
</html>

このコードをテストするには、Webブラウザーを開いて2つのボタンをクリックし、何が起こるかを観察します。しかし、シングル・ページのアプリケーションをどんどん大きくしていくと、この作業は非常に退屈になってしまいます。そのため、ブラウザー内テストに役立つフレームワークがいくつか作成されており、そのうちの1つがSeleniumと呼ばれています。

Seleniumを使用すると、Pythonでテストファイルを定義して、ユーザがWebブラウザを開いたり、ページに移動したり、ページを操作したりする様子をシミュレートできます。これを行う際のメインツールはWebドライバと呼ばれ、コンピュータ上でWebブラウザを開きます。では、このライブラリを使用してページの操作を開始する方法を見てみましょう。以下では、selenium と ChromeDriver の両方を使用します。pip install selenium を実行するとpython用にSeleniumをインストールでき、pip install chromedriver-py を実行すると ChromeDriver をインストールできます。

import os
import pathlib
import unittest

from selenium import webdriver

# Finds the Uniform Resourse Identifier of a file
def file_uri(filename):
    return pathlib.Path(os.path.abspath(filename)).as_uri()

# Sets up web driver using Google chrome
driver = webdriver.Chrome()

上記のコードは、必要な基本的な設定のすべてであるため、Pythonインタプリタを使用することで、さらに興味深い使い方ができるようになりました。最初の数行に関する注意点として、特定のページをターゲットにするには、そのページのUniform Resource Identifier (URI) が必要です。URIは、そのリソースを表す一意の文字列です。

# Find the URI of our newly created file
>>> uri = file_uri("counter.html")

# Use the URI to open the web page
>>> driver.get(uri)

# Access the title of the current page
>>> driver.title
'Counter'

# Access the source code of the page
>>> driver.page_source
'<html lang="en"><head>\n        <title>Counter</title>\n        <script>\n            \n            // Wait for page to load\n            document.addEventListener(\'DOMContentLoaded\', () => {\n\n                // Initialize variable to 0\n                let counter = 0;\n\n                // If increase button clicked, increase counter and change inner html\n                document.querySelector(\'#increase\').onclick = () => {\n                    counter ++;\n                    document.querySelector(\'h1\').innerHTML = counter;\n                }\n\n                // If decrease button clicked, decrease counter and change inner html\n                document.querySelector(\'#decrease\').onclick = () => {\n                    counter --;\n                    document.querySelector(\'h1\').innerHTML = counter;\n                }\n            })\n        </script>\n    </head>\n    <body>\n        <h1>0</h1>\n        <button id="increase">+</button>\n        <button id="decrease">-</button>\n    \n</body></html>'

# Find and store the increase and decrease buttons:
>>> increase = driver.find_element_by_id("increase")
>>> decrease = driver.find_element_by_id("decrease")

# Simulate the user clicking on the two buttons
>>> increase.click()
>>> increase.click()
>>> decrease.click()

# We can even include clicks within other Python constructs:
>>> for i in range(25):
...     increase.click()

このシミュレーションを使用して、ページの自動テストを作成する方法を見てみましょう。

# Standard outline of testing class
class WebpageTests(unittest.TestCase):

    def test_title(self):
        """Make sure title is correct"""
        driver.get(file_uri("counter.html"))
        self.assertEqual(driver.title, "Counter")

    def test_increase(self):
        """Make sure header updated to 1 after 1 click of increase button"""
        driver.get(file_uri("counter.html"))
        increase = driver.find_element_by_id("increase")
        increase.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "1")

    def test_decrease(self):
        """Make sure header updated to -1 after 1 click of decrease button"""
        driver.get(file_uri("counter.html"))
        decrease = driver.find_element_by_id("decrease")
        decrease.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "-1")

    def test_multiple_increase(self):
        """Make sure header updated to 3 after 3 clicks of increase button"""
        driver.get(file_uri("counter.html"))
        increase = driver.find_element_by_id("increase")
        for i in range(3):
            increase.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "3")

if __name__ == "__main__":
    unittest.main()

python tests.pyを実行すると、ブラウザでシミュレーションが実行され、テストの結果がコンソールに出力されます。コードにバグがあってテストに失敗した場合の例を次に示します。

failed selenium test

CI/CD

CI/CD (Continuous Integration and Continuous Delivery) は、ソフトウェア開発のベスト・プラクティスのセットであり、チームがコードをどのように作成し、そのコードを後でアプリケーションのユーザにどのように配布するかを決定します。名前が示すように、このメソッドは次の2つの主要部分で構成されています。

  • 継続的な統合:
    • メインブランチへの頻繁なマージ
    • 各マージでのユニットテストの自動化
  • 継続的な提供:
    • 短いリリーススケジュール。アプリケーションの新しいバージョンが頻繁にリリースされることを意味します。

CI/CDがソフトウェア開発チームの間でますます一般的になってきた理由はいくつかあります。

  • 異なるチームメンバーが異なるフィーチャで作業している場合、複数のフィーチャを同時に結合すると、多くの互換性の問題が発生する可能性があります。継続的インテグレーションによって、小さなコンフリクトが発生しても、チームはそれに対処することができます。
  • ユニットテストは各マージで実行されるため、テストが失敗したときに、問題の原因となっているコードの部分を切り分けることが容易になります。
  • アプリケーションの新バージョンを頻繁にリリースすることで、問題がローンチ後に発生した場合に、開発者は問題を切り分けることができます。
  • 小さな段階的な変更をリリースすることで、ユーザは全く違うバージョンに圧倒されることなく、新しいアプリの機能にゆっくりと慣れることができます。
  • 新機能のリリースを待たずに、企業は競争の激しい市場で優位を保つことができます。

GitHub Actions

継続的インテグレーションを支援するために使用される一般的なツールの1つにGitHub Actionsがあります。GitHub Actionsではワークフローを作成して、誰かがgitリポジトリにプッシュするたびに実行するアクションを指定することができます。例えば、スタイルガイドが守られているかどうか、あるいは一連のユニットテストがパスしているかどうかを、プッシュのたびにチェックしたい場合があります。

GitHubアクションを設定するために、YAMLという設定言語を使用します。YAMLはキーと値のペア (JSONオブジェクトやPython Dictionaryのような) を中心にデータを構造化します。簡単なYAMLファイルの例を次に示します。

key1: value1
key2: value2
key3:
    - item1
    - item2
    - item3

次に、GitHub Actions で動作する YAML ファイル ( name.yml または name.yaml の形式をとります) の設定例を見てみましょう。これを行うには、リポジトリに .github ディレクトリを作成し、その中に workflows ディレクトリを作成し、最後にその中に ci.yml ファイルを作成します。このファイルには、次のように記述します。

name: Testing
on: push

jobs:
  test_project:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run Django unit tests
      run: |
        pip3 install --user django
        python3 manage.py test

GitHub Actionsを使うのはこれが初めてなので、このファイルの各部分が何をしているかを見てみましょう。

  • まず、ワークフローに名前 name を付けます。ここでは、 「Testing」 とします。
  • 次に、on キーを使用して、ワークフローを実行するタイミングを指定します。ここでは、誰かがリポジトリにプッシュするたびにテストを実行します。
  • ファイルの残りの部分は jobs キーに含まれています。jobsキーは、送信のたびに実行されるジョブを示します。
    • この例では、唯一のジョブは test_project です。すべてのジョブは2つのコンポーネントを定義する必要があります。
      • runs-on キーは、コードを実行するGitHubの仮想マシンを指定します。
      • steps キーは、このジョブの実行時に実行されるアクションを提供します。
        • uses キーで、使用するGitHubアクションを指定します。actions/checkout@v2 はGitHubによって書かれたアクションで、使用することができます。
        • ここで name キーを使用すると、実行するアクションの説明を表示できます。
        • run キーの後に、GitHubのサーバで実行したいコマンドを入力します。ここでは、Djangoをインストールしてからテスト・ファイルを実行します。

それでは、GitHubでリポジトリを開き、ページの上部近くにあるいくつかのタブを見てみましょう。

  • Code:これは、ディレクトリ内のファイルやフォルダを表示できるため、最も頻繁に使用するタブです。
  • Issues:ここでは、バグ修正や新機能の要求などの問題を開いたり閉じたりできます。これは、アプリケーションのTo-Doリストと考えることができます。
  • Pull Requests:あるブランチのコードを別のブランチにマージしたい人からのリクエスト。これは便利なツールであり、コードがマスターブランチに統合される前に、コードレビューを行ってコメントを付けたり提案をしたりすることができる。
  • Actions:継続的インテグレーションを行うときに使うタブで、プッシュのたびに実行されたアクションのログを提供します。

ここでは、airport プロジェクト内の models.pyis_valid_flight 関数のバグを修正する前に、変更をプッシュしたとします。これで GitHub Actions タブに移動し、最新のプッシュをクリックし、失敗したアクションをクリックし、ログを見ることができます。

action

さて、バグを修正した後、より良い結果を得ることができました。

action success

Docker

ソフトウェア開発の世界では、コンピュータ上の構成がアプリケーションの実行時の構成と異なる場合に問題が発生することがあります。異なるバージョンのPythonや、サーバ上でクラッシュしてもコンピュータ上でアプリケーションをスムーズに実行できるようにする追加パッケージがインストールされている可能性があります。これらの問題を回避するには、プロジェクトで作業する全員が同じ環境を使用していることを確認する方法が必要です。これを行う1つの方法は、Dockerと呼ばれるツールを使用することです。これは、コンテナ化ソフトウェアであり、多くの共同作業者やサイトが実行されているサーバ間で標準化できる、コンピュータ内に分離された環境を作成します。Dockerは仮想マシンに少し似ていますが、実際には異なる技術です。仮想マシン (GitHub Actionsや AWS サーバを構築する時に使われるようなもの) は、事実上、独自のオペレーティングシステムを備えた仮想コンピュータ全体です。つまり、仮想マシンが実行されているすべての場所で多くの領域を消費することになります。一方、Dockerは、既存のコンピュータ内にコンテナを設定することで動作するため、スペースを節約できます。

Dockerコンテナの概念がわかったので、コンピュータ上でDockerコンテナを設定する方法を見てみましょう。これを行うための最初のステップは、Dockerfile という名前のDockerファイルを作成することです。このファイルでは、コンテナに含めるライブラリとバイナリを記述したDocker Imageの作成方法について説明します。Dockerfile の例を以下に示します。

FROM python:3
COPY .  /usr/src/app
WORKDIR /usr/src/app
RUN pip install -r requirements.txt
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]

ここでは、上記のファイルが実際に何をするのかを詳しく見ていきます。

  • FROM python3:これは、Python 3がインストールされている標準イメージに基づいてこのイメージを作成していることを示しています。これはDockerファイルを書くときによくあることで、新しいイメージを作るたびに同じ基本設定を再定義する手間を省くことができます。
  • COPY . /usr/src/app:これは、現在のディレクトリ (. )の全てを 、新しいコンテナの /usr/src/app ディレクトリにコピーします。
  • WORKDIR /usr/src/app:コンテナ内でコマンドを実行する場所を設定します (ターミナルの cd に少し似ています) 。
  • RUN pip install -r requirements.txt:この行では、requirements.txt というファイルにすべての要件を含めていると仮定して、すべての要件がコンテナ内にインストールされます。
  • CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]:最後に、コンテナーの起動時に実行するコマンドを指定します。

これまでこのクラスでは、Djangoのデフォルトのデータベース管理システムとしてSQLiteのみを使用してきました。しかし、実際のユーザが実際に使用しているアプリケーションでは、SQLiteは他のシステムほど簡単にスケールできないため、ほとんど使用されません。ありがたいことに、データベース用に別のサーバを実行したい場合は、Dockerコンテナをもう1つ追加し、Docker Composeと呼ばれる機能を使ってそれらを一緒に実行することができます。これにより、2つの異なるサーバを別々のコンテナで実行できますが、互いに通信することもできます。これを指定するには、docker-compose.yml というYAMLファイルを使用します。

version: '3'

services:
    db:
        image: postgres

    web:
        build: .
        volumes:
            - .:/usr/src/app
        ports:
            - "8000:8000"

上記のファイルは、次のことを行います。

  • Docker Compose のバージョン3を使用していることを指定します。
  • 次の2つのサービスの概要を示します。
    • db は、Postgresがすでに作成したイメージに基づいてデータベースコンテナを設定します。
    • web はDockerに次のように指示してサーバのコンテナをセットアップします。
      • 現在のディレクトリ内でDockerfileを使用します。
      • コンテナ内の指定されたパスを使用します。
      • コンテナ内のポート8000をコンピュータのポート8000にリンクします。

これで、docker-compose up コマンドでサービスを開始する準備ができました。これにより、新しいDockerコンテナ内で両方のサーバが起動されます。

この時点で、Dockerコンテナ内でコマンドを実行してデータベースエントリを追加したり、テストを実行したりすることができます。これを行うには、まず docker ps を実行して、実行中のすべての docker コンテナを表示します。次に、入力したいコンテナの CONTAINER ID を見つけ、docker exec -it CONTAINER_ID bash -l を実行します。これで、コンテナ内に設定した usr/src/app ディレクトリに移動します。コンテナ内で必要なコマンドを実行し、CTRL-D を実行して終了します。

これでこのレッスンは終わりです!次回は、プロジェクトの規模を拡大し、それらがセキュアであることを確認します。