Django と Channelsでチャット作成のチュートリアルをやってみる(Tutorial Part 4: Automated Testing)
Channelsのチュートリアルシリーズ最後!
今回は前章までで作成したもののテストコードを書く
前章までの記事は下記
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')
ChannelsLiveServerTestCase
はDjangoのE2Eテストツール(StaticLiveServerTestCase
やLiveServerTestCase
)を拡張したもの
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でできることをしらべないといけないかなーと...。