「画像でゴミ分類！」アプリ作成日誌day9～テストの追加～

# 一言でいうと
Djangoで作成したアプリにSeleniumを使ったテストを追加しました！

# これまでの経緯
「画像でゴミ分類！」という画像を入れたらそれが何ゴミか分類してくれるwebアプリを作成しました。いったん完成していたのですが、テストが大事とかテスト駆動開発いいよねという記事に感化されてこのアプリにもテストを導入します。



＜これまでの記事一覧＞

- [「画像でゴミ分類！」アプリ作成日誌day1～データセットの作成～](https://qiita.com/eycjur/items/7d8223b28758c7dfaaa0)
- [「画像でゴミ分類！」アプリ作成日誌day2～VGG16でFine-tuning～](https://qiita.com/eycjur/items/3e954cb70dc15f996c2d)
- [「画像でゴミ分類！」アプリ作成日誌day3～Djangoでwebアプリ化～](https://qiita.com/eycjur/items/9c618538177c82f7fdc3)
- [「画像でゴミ分類！」アプリ作成日誌day4～Bootstrapでフロントエンドを整える～](https://qiita.com/eycjur/items/7b58c28eb8b16e722b5d)
- [「画像でゴミ分類！」アプリ作成日誌day5～Bootstrapでフロントエンドを整える2～](https://qiita.com/eycjur/items/08808097acd625e00652)
- [「画像でゴミ分類！」アプリ作成日誌day6～ディレクトリ構成の修正～](https://qiita.com/eycjur/items/f436787c2d517af6cad6)
- [「画像でゴミ分類！」アプリ作成日誌day7～サイドバーのスライドメニュー化～](https://qiita.com/eycjur/items/009dca5866c3e9ad0dd6)
- [「画像でゴミ分類！」アプリ作成日誌day8～herokuデプロイ～](https://qiita.com/eycjur/items/ebfae5db5dd1cd8349ea)

# テスト手法
Seleniumを使ったUIテストを行いたいので、DjangoのLiveServerTestCaseを利用します。

# Jupyterで動作確認
Seleniumについてまだ勉強中なので、いきなりwebアプリ上に書くのではなく、Jupyterで挙動を確認しながらコードを書いていきます。

まずは、chromeを起動しましょう

```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

DRIVER_PATH = r'chromedriver.exeのパス'

# ブラウザの起動
driver = webdriver.Chrome(executable_path=DRIVER_PATH)
```

そして開発用サーバーにアクセス。ここは開発用サーバーを立ち上げておく必要があります。

```python
driver.get('http://localhost:8000/garbage')
```

まずは、タイトルを取得してみましょう

```python
driver.title
# '画像でゴミ分類！'
```

次に、要素を取得してみましょう

```python
driver.find_element_by_class_name("sample-img").get_attribute('outerHTML')
# '<img src="/static/garbage/media/gif/waiting.278078bec7bd.gif" alt="画像1" class="mt-3 sample-img waiting" id="waiting-gif">'
```
outerHTMLを見ると無事取得できていることがわかります。ただ、実はこのクラスに所属するものは複数あるので、そんなときは`find_elements_by_class_name`と複数形で使うとリストで取得できます。

```python
len(driver.find_elements_by_class_name("sample-img"))
# 3
```

では画像をクリックする処理を書きます

```python
driver.find_elements_by_class_name("sample-img")[1].click()
```
簡単ですね。

今度は遷移先のページでタグを取得してみます。

```python
num_tr = len(driver.find_elements(By.XPATH, '//tr'))
```
書き方がちょっと変わりましたが、XML Pathで要素を指定しているだけで、やってることは同じですね。

これを用いてテストするには以下のようにすればいいです。

```python
assert 6 == num_tr, "表の行数が一致しません"
```



では、一度トップページに戻って文字検索時のテストを書いていきます。
まずは、検索ボックスを取得してみましょう

```python
driver.get('http://localhost:8000/garbage')
driver.find_elements(By.XPATH, '//input[@name="word"]')
# [<selenium.webdriver.remote.webelement.WebElement (session="d795b92e9edbbd43c50a2be08e0b75e1", element="54b9e56a-2624-4a33-9f5f-be8fe77a6e5e")>,
#  <selenium.webdriver.remote.webelement.WebElement (session="d795b92e9edbbd43c50a2be08e0b75e1", element="1ccd154e-326f-4e5c-8004-9258ec03bb39")>]
```

あれ、2つ出てくるのはなんででしょうか。これは、スマホ対応時のサイドバーとPC時の左メニューの分ですね。

PC時の左メニューのほうだけを使います。文字列として針を入力してみましょう。

```python
search_box = driver.find_elements(By.XPATH, '//input[@name="word"]')[1]
search_box.send_keys('針')
```

そして、Enterキーを押します。この後は先ほどと同じく表の行数をチェックするようにしようかと思います。

```python
search_box.send_keys(Keys.ENTER)
len(driver.find_elements(By.XPATH, '//tr'))
# 14
```

# testコードの追加
これまで書いたコードをwebアプリ上に移植します。
まず、test.pyを削除してその代わりにtests/test_views.pyを追加します。
まずはおまじないです。LiveServerTestCaseクラスをオーバーライドしてドライバーなどの場所を教えます。

```python
from django.test import LiveServerTestCase
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys


class TestClickSample(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver(executable_path=r'garbage\tests\chromedriver.exe')

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()
```

まずは、ページを開いてタイトルがあってるかを確認をしておきましょう。

```python
    def test_open_page(self):
        """ページを開いてタイトルがあってるかを確認
        """
        self.selenium.get('http://localhost:8000/garbage')
        self.assertEquals("画像でゴミ分類！", self.selenium.title)
```

次にサンプル画像をクリックしたときの処理を記述して、モデルの挙動が正しいかを確認できるようにします。

```python
    def test_click_sample(self):
        """モデルの動きが正常かを確認
        サンプル画像をクリックした際の挙動を確認
        """
        self.selenium.get('http://localhost:8000/garbage')
        self.selenium.find_elements_by_class_name("sample-img")[1].click()

        num_tr = len(self.selenium.find_elements(By.XPATH, '//tr'))
        self.assertEquals(6, num_tr)
```

最後に文字検索時の挙動をテストします。

```python
    def test_search(self):
        """文字検索時の挙動をテスト
        """
        self.selenium.get('http://localhost:8000/garbage')
        search_box = self.selenium.find_elements(By.XPATH, '//input[@name="word"]')[1]
        search_box.send_keys('針')
        search_box.send_keys(Keys.ENTER)

        num_tr = len(self.selenium.find_elements(By.XPATH, '//tr'))
        self.assertEquals(14, num_tr)

```

テスト実行してみましょう。

```console
Creating test database for alias 'default'...
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 3 tests in 10.380s

OK
```

無事テストが通ったようです！

# 参考文献
- 大高 隆(2019)『動かして学ぶ！Python Django開発入門』翔泳社
- [Djangoを始めよう！ 〜チュートリアル⑤〜](https://qiita.com/tfrcm/items/1b6079bec064a4e8b211)
- [Seleniumで要素を選択する方法まとめ](https://qiita.com/VA_nakatsu/items/0095755dc48ad7e86e2f)