@Satoh_D no blog

大分にUターンしたので記念に。調べたこととか作ったこととか食べたこととか

Django と Channelsでチャット作成のチュートリアルをやってみる(Tutorial Part 4: Automated Testing)

Channelsのチュートリアルシリーズ最後!
今回は前章までで作成したもののテストコードを書く

前章までの記事は下記

satoh-d.hatenablog.com

https://satoh-d.hatenablog.com/entry/2020/10/26/180000satoh-d.hatenablog.com

https://satoh-d.hatenablog.com/entry/2020/10/27/180000satoh-d.hatenablog.com

前提

  • Django: 2.2.8(本当はDjang 3.0+がいいけどローカルがこれだったので。。)
  • Channels: 2.4.0
  • Redis-server: 5.0.9
  • Google Chrome: 86.0.4240.111(Headless Chromeで利用)
  • ChromeDriver: 86.0.4240.22
  • Selenium: 3.141.0

Chrome, ChromeDriver, Seleniumのインストール

それぞれのインストール方法は次の通り(今回はDockerコンテナ上で実施)

# Google Chroome
$ wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add
$ echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list
$ apt-get update
$ apt-get install -y google-chrome-stable
$ google-chrome --version
Google Chrome 86.0.4240.111 

# Chrome Driver
$ curl -OL https://chromedriver.storage.googleapis.com/76.0.3809.126/chromedriver_linux64.zip
$ unzip chromedriver_linux64.zip chromedriver
$ mv chromedriver /usr/bin/chromedriver

# Selenium
$ pip install selenium

テストコードは下記の通り
Headless Chromeを定義する際にOptionを指定しないとテスト実施時にエラーとなるため注意すること

# chat/tests.py
from channels.testing import ChannelsLiveServerTestCase
from selenium import webdriver
# Tutorialに載ってないけどOptionsを定義しないとテスト実施時にエラーとなる
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.wait import WebDriverWait

class ChatTests(ChannelsLiveServerTestCase):
    serve_static = True  # emulate StaticLiveServerTestCase

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # optionsはTutorialに載ってないけど定義しないとテスト実施時にエラーとなる
        options = Options()
        options.add_argument('--no-sandbox')
        options.add_argument('--headless')
        try:
            # NOTE: Requires "chromedriver" binary to be installed in $PATH
            cls.driver = webdriver.Chrome(options=options)
        except:
            super().tearDownClass()
            raise

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()
        super().tearDownClass()

    def test_when_chat_message_posted_then_seen_by_everyone_in_same_room(self):
        try:
            self._enter_chat_room('room_1')

            self._open_new_window()
            self._enter_chat_room('room_1')

            self._switch_to_window(0)
            self._post_message('hello')
            WebDriverWait(self.driver, 2).until(lambda _:
                'hello' in self._chat_log_value,
                'Message was not received by window 1 from window 1')
            self._switch_to_window(1)
            WebDriverWait(self.driver, 2).until(lambda _:
                'hello' in self._chat_log_value,
                'Message was not received by window 2 from window 1')
        finally:
            self._close_all_new_windows()

    def test_when_chat_message_posted_then_not_seen_by_anyone_in_different_room(self):
        try:
            self._enter_chat_room('room_1')

            self._open_new_window()
            self._enter_chat_room('room_2')

            self._switch_to_window(0)
            self._post_message('hello')
            WebDriverWait(self.driver, 2).until(lambda _:
                'hello' in self._chat_log_value,
                'Message was not received by window 1 from window 1')

            self._switch_to_window(1)
            self._post_message('world')
            WebDriverWait(self.driver, 2).until(lambda _:
                'world' in self._chat_log_value,
                'Message was not received by window 2 from window 2')
            self.assertTrue('hello' not in self._chat_log_value,
                'Message was improperly received by window 2 from window 1')
        finally:
            self._close_all_new_windows()

    # === Utility ===

    def _enter_chat_room(self, room_name):
        self.driver.get(self.live_server_url + '/chat/')
        ActionChains(self.driver).send_keys(room_name + '\n').perform()
        WebDriverWait(self.driver, 2).until(lambda _:
            room_name in self.driver.current_url)

    def _open_new_window(self):
        self.driver.execute_script('window.open("about:blank", "_blank");')
        self.driver.switch_to_window(self.driver.window_handles[-1])

    def _close_all_new_windows(self):
        while len(self.driver.window_handles) > 1:
            self.driver.switch_to_window(self.driver.window_handles[-1])
            self.driver.execute_script('window.close();')
        if len(self.driver.window_handles) == 1:
            self.driver.switch_to_window(self.driver.window_handles[0])

    def _switch_to_window(self, window_index):
        self.driver.switch_to_window(self.driver.window_handles[window_index])

    def _post_message(self, message):
        ActionChains(self.driver).send_keys(message + '\n').perform()

    @property
    def _chat_log_value(self):
        return self.driver.find_element_by_css_selector('#chat-log').get_property('value')

ChannelsLiveServerTestCaseDjangoのE2Eテストツール(StaticLiveServerTestCaseLiveServerTestCase)を拡張したもの
Channlesで定義した内部のルーティング(/ws/room/ROOM_NAMEのような)をいい感じにテストしてくれるとか

テストコード実行時にテスト用DBを自動で作成するため、権限を付けてあげる
今回は(MySQL, DB名: test_django(デフォルト))

mysql> grant all privileges on test_django.* to 'user'@'%';

実際にテストを動かしてみる
「OK」とでればテストが通ったことになる

$ python manage.py test chat.tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 2.818s

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

最後に

Tutorialと環境が違う(そもそもDjangoのバージョンがちがったり色々...)ことでハマってしまうところが多々あった。
とはいえなんとか終わらせることができてよかった。
もうすこしWebSocketでできることをしらべないといけないかなーと...。

参考サイト