WebアプリのテストでUIテストと統合テストを切り離そうとしたけど、ちょっと考えなければならなかったハナシ

 Webアプリのテストで、Seleniumを使ったUIテストを書いておいた。テスト内容として、フォームを埋めて、POSTでコントローラが意図通りに動いて、のぞんだレスポンスが返ってくるかまでテストしていた。UIテストと同時に統合テストの役割をになっていたので、役割をフォームのテスト程度に抑えて、別の統合テストを用意したかった。だからPython + requests( +lxml)で、直接RESTリクエストを投げてコントローラを動かすテストを書いてみた。
https://github.com/hMatoba/tetsujin/blob/master/tetsujin/PythonIntegtationTest/tests/master_test.py


 ただGETを投げてレスポンスを確認するようなテストもSeleniumでやっていたのだが、これをrequests + lxmlにしたところパフォーマンスは数十倍を超えたのでまあいい手ごたえ。ただSeleniumを使ったテストでも、一つのメソッドにつき一秒もかかっていないのでそんな神経質にならんでもいいかとも思う。
 考えなければならんかったのはPOSTを行うフォームのテスト。POSTする内容をrequestsで作る。それをPOSTで投げる。なのだが、フォームなのでもちろんCSRF対策が入っていて、これを考慮せんといかんかった。
 ASP.NET Core MVCでのCSRF対策は備え付けのものを使った。
Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core
CSRF対策のトークンを確認したところ、フォームには”__RequestVerificationToken”の名前で、クッキーには”.AspNetCore.Antiforgery.9TtSrW0hzOs”のキーで値が入っていた。だが、長さ190文字中、先頭30文字弱のみ一致しており、それ以降は異なっていた。これに関して自分で適当に作ってフォームとCookieにセットするのはやめておいたほうがよさそう。ここはSeleniumを使ってブラウザでフォームを動かしたほうが、自分でごにょごにょやるより手っ取り早いだろう。

 まとめ。WebアプリのUIテストと統合テストをわけたかったので、RESTリクエストを作ってコントローラを動かすテストを追加してみた。GETでレスポンス確認する場合は狙い通りだった。いっぽうでPOSTでCSRF対策が入っている場合、フォームやクッキーにその対策の対策を入れる必要に気づいた。そうすると、そこのあたりはSeleniumを使ってブラウザを動かすテストにしておくのが余計なことを考えずに済んで手っ取り早いだろうという知見を得た。
comment: 0

Seleniumで行っていたテストをPageObjectデザインパターンに書き換えてみる

 あるWebアプリのログインの成功と失敗を、Seleniumでテストしていた。HTML要素をIDから選択し、キーボード入力のエミュレートでログインIDとパスワードを入れて、と生々しいコードを書いていた。
 このたびはSeleniumでのテストにはPageObjectデザインパターンが推奨されているというのを知って、おもしろそうなので適用してみた。

 ログインの成功をテストしていた元のコード。若干生々しいDOMの操作をしている。
def test_login_success(self):

"""login success"""
self.driver.get(HOST + "/Auth/Login")

el1 = self.driver.find_element_by_name("_id")
el1.send_keys("testuser")

el2 = self.driver.find_element_by_name("password")
el2.send_keys("password")

el3 = self.driver.find_element_by_name("enter")
el3.click()

self.assertIn("Admin Page", self.driver.title)


 PageObjectデザインパターンの適用として、ログインフォームの入力からPOSTまでを行うクラスを用意する。
class LoginFormStory:

def __init__(self, driver):
driver.get(HOST + "/Auth/Login")
self._driver = driver

def enter_id(self, id_):
el = self._driver.find_element_by_name("_id")
el.send_keys(id_)
return self

def enter_password(self, password):
el = self._driver.find_element_by_name("password")
el.send_keys(password)
return self

def post_form(self):
el = self._driver.find_element_by_name("enter")
el.click()
return self

def is_authenticated(self):
if "Admin Page" in self._driver.title:
return True
else:
return False


 上記のクラスの用意によって、テストの進行をメッセージングで表現できる。
def test_login_success(self):

"""login success"""
is_authenticated = (
LoginFormStory(self.driver)
.enter_id("testuser")
.enter_password("password")
.post_form()
.is_authenticated()
)
self.assertTrue(is_authenticated)


可読性がめっちゃいい感じ。気に入った。
comment: 0

このブログについて

ソース: https://github.com/hMatoba/tetsujin

 ブログのバックエンドを作るのが趣味になっているような気がする。
 はじめはPython on GoogleAppEngineで作って、それのデータベースをNoSQLにしてみたらどうだとか始めた。そのあとでTornadoやFlask、BottleといったPythonのフレームワークをいろいろやってみて、Node.jsはasync,awaitが入る前にやってちょっときついと思ってストップした。
 その後WPFで好きだったC#で作ってみたくなって、.NET Coreのリリース直後にAzureに乗るものを作った。そして今回、.NET Core 2.0のリリースがあったのを契機にDocker運用するものに作り直した。

 ASP.NET Core MVCを使ってみた感想。モデル、ビュー(Razor)、コントローラのみを使うような、SinatraやFlaskのような、うっすい使い方もできる。スキャフォルディングとかあるけど無理に使う必要はない。フレームワークに縛られてるような感覚は強くない。その点、うまくやれるかは書き手次第。


 このブログはタグに並べた要素が全部盛りで実装されている。
 C#が好きなので、それがオープンソースになったのがうれしかった。だからほかのオープンソース技術をメインに組み合わせて、C#はMSの用意したセット(WindowsOSでIISとかSQLServerとかって構成)じゃないと使えないなんてことはないというのを実践してみたかった。DebianでNginx、MongoDBなどって構成。
 使用サービスの構成としては、Github、TravisCI、Docker Hub、Hyper.shが主なところ。
・Github→バージョン管理
・TravisCI→CIサービス
・Docker Hub→ビルドしたコンテナイメージの置き場所
・Hyper.sh→Docker開発者が「これほぼDockerやんけ」と言うぐらいにDockerライクなコンテナホスティングサービス。もちろんDockerで作ったコンテナをホスティングできる
 使用サービスの構成はシンプルなほうがいいという一般論も存在するだろうけど、これはこれで楽をするための構成を敷いている。Dockerを使ったサーバの構成管理、自動テスト、CIサービス上での自動テストからの自動デプロイなどなど。アプリの開発継続に注力できる。

 Githubへのプッシュで、TravisCIでのビルド、テストが行われる。テストにパスしてなおかつブランチがmasterだったらならDocker Hubへビルドされたコンテナイメージがアップされる。そののち、Hyper.shでそのコンテナイメージをプル、デプロイまでのコマンドが自動で流れる。Docker Hubを経由しているので、コンテナイメージがビルドの産物として残るようになっている。


・運用お値段
 VPSは安い。でもOSの管理が必要だからヤダ。AWSとかAzureのイカしたPaasを使いたいけど高い。
 DockerコンテナホスティングをしてくれるHyper.shというサービスがあり、これで月2000円以下で運用できている。リバースプロキシコンテナ+アプリコンテナの2コンテナをランニングさせるコスト1000円に加え、個人的にいろんなアプリで使うMongoDBを仕方なくVPSで運用しているコスト600円程度となっている。VPSとメジャーどころのPaasの中間あたりのお値段。
 問題としてHyper.shが日本にもアジアにもリージョンがないことによるレイテンシがががというのがあるけども妥協。


・構成
 ファイルはAzureStorage
 ↑にも書いたけどコンテナホスティングで運用していて、走っているコンテナはリバースプロキシ用一台とアプリ用一台
 証明書はLet's Encrypt
 コンテナのOSは、リバースプロキシのほうがAlpine、アプリはDebian
 テストはxUnitによる単体テストが実行できるようにしてある
 それに加えてSeleniumでのE2Eテストも用意してある。Seleniumは、一度C#で動かしたらきつかったのでPythonで動かしている
 バージョン管理はGithub
 Githubへのデプロイで、TravisCIでのデプロイが走る
 TravisCIではとりあえずSeleniumでE2Eテストをやっている
 TravisCIでテストに成功したら、そこからHyper.shへ自動デプロイが始まる
 ただし自動デプロイが始まるのはmasterブランチへのコミット時のみ


 いろいろ構成していて半分は趣味で試したかったものであるが、同時にデプロイの自動化という一年前から突き詰めたかった課題への挑戦もしている。これのおかげで、今後このブログエンジンを改良していくのにせっせと手作業でデプロイをする必要がなくなった。


・なおしたい点
 dotnetコマンドが現状docker composeを使ったプロジェクトをビルドしてくれない。だから一部プロジェクトでCI環境でのビルドができず、開発環境でビルドしたものをDocker Hub経由で使っている。CI環境でビルドができないのはSDKの不足が原因。そのSDKはVisualStudio Communityに用意されているのだが、SDKがオープンソースになっていないという理由でdotnet CLIに入れるかどうかもたついている。
'17/9/28追記
docker composeが入っているプロジェクトのビルド方法を変えて対応完了

comment: 0

.NET CoreのDockerコンテナに入ったWebアプリをTravisCIでCI

 VisualStudioで作成された.dcprojをdotnetコマンドでビルドしようとすると、"Microsoft.Docker.Sdkのほにゃららがない"と言われる。
The imported project "C:\Program Files\dotnet\sdk\1.0.0\Sdks\Microsoft.Docker.Sdk\Sdk\Sdk.props" was not found.


これを言われるのはWindows環境でもLinux環境でも。dotnet cliでイシューとして挙げられている。
VS2017 Docker-Compose Project breaks build on command line
必要なSDKはdotnet cliのディレクトリにはないが、VisualStudioのディレクトリには入っている。これを使えばまあできんこともないが、一時的な策になってしまう。

 dotnetコマンドで.dcprojをビルドするのはここではあきらめる。
 そもそもぼくはなにがしたいのか。VisualStudioで作ったdocker-compose入りのプロジェクトをビルドしたかった。TravisCI上でdotnetコマンドでプロジェクトをビルドして、そこから出てきたDockerイメージをDocker hubへプッシュしたかった。

 まあそもそも.dcprojのビルドは、dotnetコマンドでは無理だがVisualStudioを使えばビルドできる。それを踏まえて。
代替案
・必要なファイルを開発環境のWindowsで用意してTravisCIへ送って、DockerイメージをTravisCIでビルドしてテストがうまくいったら、Docker hubへプッシュ
・開発環境でVisualStudioでビルドしたDockerイメージをTravisCIでプルしてテスト。テストに通ったらDockerイメージをリネームしてプッシュ

 一つ目。デフォで必要なビルドしてファイルがgitでignoreされているので、ここのデフォ設定をいじらなければならない。その結果、変更を追いかけるファイルが増えてアレ
 二つ目。手っ取り早い。これでいいや。まあ暫定なので、.dcprojがビルドできないバグが直ったら改めて考える。


 一通りやってみたGithubリポジトリ↓
https://github.com/hMatoba/DNCinDocker
ロクに参考資料がないのをトライアンドエラーでやったので、コミットタイトルは超適当。

 .NET CoreのWebアプリが作成できたらリリース設定でプロジェクトをビルドし、WebアプリのDockerイメージを得る。Dockerイメージには"ci"というタグをつけておくことにする。そのイメージをDocker hubへプッシュしておく。

 Docker hubにプッシュしてあるイメージを使ったTravisCIでのテストを用意する。TestWithSeleniumというプロジェクトでSeleniumを使ったテストを用意した。これをdocker-composeで管理する。TravisCI上で使うためのdocker-composeファイルを用意する。
docker-compose.travis.yml

version: '2'

services:
dncindocker:
image: matoba/dncindocker:ci
ports:
- "8080:80"
selenium:
image: selenium/standalone-chrome
ports:
- "4444:4444"
links:
- dncindocker:webapp

 上記を使ってTravisCI上でテストを走らせる。
language: csharp

mono: none
dotnet: 1.0.4
dist: trusty
services:
- docker

env:
- DOCKER_COMPOSE_VERSION=1.8.0

before_install:
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- docker-compose -f docker-compose.travis.yml config
- docker-compose -f docker-compose.travis.yml build
- docker-compose -f docker-compose.travis.yml up -d
- sudo pip install selenium

script:
- cd ./TestWithSelenium
- python setup.py test

after_success:
- docker tag matoba/dncindocker:ci matoba/dncindocker:latest
- docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}"
- docker push matoba/dncindocker:latest
matrix:
fast_finish: true

 成功後にDockerイメージをリネームしてプッシュするので、Docker hubのIDとパスワードを使わなければならない。これは環境変数で埋め込む。

 一通りファイルがそろったのでGithubへプッシュしてみる。そのままTravisCIへ流れるように設定してある。







TravisCIでテストはとおっている様子。




Docker hubに、開発環境からプッシュされたciタグのイメージと、TravisCIからプッシュされたlatestタグのイメージが上がっている。

 ちなみにTravisCIで行っているコードの中身としては、必要なもののインストールとテストだけである。必要なものはdocker-composeとPythonのSeleniumドライバだけ。あとのテストコードはdocker-composeとPythonで処理される。つまり、このテストはWindowsでも実行できる。開発環境でもテストを検証できるってこと。便利。
comment: 0

TravisCIでDockerに入ったWebアプリのSeleniumテスト

 WebアプリをDockerでビルドして、そこでできたイメージをデプロイしてコンテナを走らせるってケースは増えてきているはず。そのイメージを使ってTravisCI上でSeleniumを使ってテストしてみる。


 Dockerイメージは既にDocker Hubにアップしてあるものとする。今回使うのは、Webアプリで、リクエストを送ると世界へのあいさつを返すだけのごく初歩的なものだ。テストはSeleniumを使ってリクエストを送り、レスポンスのHTMLのタイトルをチェックするものとする。

 テストに使うSeleniumを用意する。Seleniumはホスト環境に用意するのもいいが、仮想ディスプレイを使ったりすると、自分の環境でTravisCIでやるコマンドをちょっと試したかったりする場合に用意が手間になる。SeleniumもDockerで公式のものが用意されているので、今回はDockerで用意されたものを使う。

 Seleniumを動かすテストコードは、今回はPythonを使う。
 DockerコンテナとしてWebアプリとSeleniumが動いており、ホストにSeleniumを動かすPythonがあるというのが今回の環境。コンテナが二つあるので、ここらはdocker-composeを使うことにする。

 まずWebアプリとSeleniumのある環境の準備のためにdocker-compose。docker-compose.ymlを用意する。
docker-compose.yml

version: '2'

services:
tornadoapp:
image: matoba/tornadoapp
ports:
- "80:80"
selenium:
image: selenium/standalone-chrome
ports:
- "4444:4444"
links:
- tornadoapp

 WebアプリはTornadoを使って作ったのでtornadoappという名前にしてある。ポートバインドで、ホストのポート80でこのWebアプリがlistenされることになっている。
 あとSeleniumは、Pythonドライバで接続するためにポート4444でlistenしているのと、Webアプリに名前でアクセスができるようにlinksで設定してある。
 環境はこれでOK。

 ホストで動く、Seleniumを動かすPythonを用意する。"python setup.py test"のコマンドで動かすので、setup.pyと実際のテストコードを用意する。./TestWithSelenium以下にsetup.pyと、tests/s_test.pyが必要。
setup.py

from setuptools import setup
import sys

sys.path.append('./tests')

setup(
name='browsertest',
version='1.0',
description='test a project on browser',
test_suite = 's_test.suite',
install_requires=[
'selenium',
],
)


s_test.py

import unittest

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

#HOST = "http://172.17.0.1"
HOST = "http://tornadoapp"

class BrowserTests(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Remote(
command_executor = 'http://127.0.0.1:4444/wd/hub',
desired_capabilities = DesiredCapabilities.CHROME
)
self.driver.implicitly_wait(10)

def test_top(self):
"""top page"""
self.driver.get(HOST + "/")
self.assertIn("foo", self.driver.title)

def suite():
"""run tests"""
suite = unittest.TestSuite()
suite.addTests([unittest.makeSuite(BrowserTests)])
return suite


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

 SeleniumからWebアプリへのアクセスはdocker-composeで設定した名前で行っている。その他の手段としては、ポートがホストにバインドされていれば、IPでアクセスできる。Dockerネットワークのデフォルトは172.17.0.*なのでこれを使っていいはず。

 テストが用意できたら、あとはこれをTravisCIで動かすための設定ファイルを用意する。
.travis.yml

sudo: required

language: python

python:
- 3.6

services:
- docker

env:
- DOCKER_COMPOSE_VERSION=1.8.0

before_install:
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- docker-compose build
- docker-compose up -d
- pip install selenium

script:
- cd ./TestWithSelenium
- python setup.py test

matrix:
fast_finish: true

 やっていること↓
前準備
・Dockerを使えるように
・docker-composeコマンドを使えるように
・WebアプリコンテナとSeleniumコンテナを用意
・PythonのSeleniumドライバを用意
テスト
・Pythonコードを走らせる

 これでTravisCIでコンテナに入ったWebアプリを、Seleniumでテストする準備ができた。あとはアップするだけ。設定などを間違えていなければこれでテストは通る。




今回のTravisCI
https://travis-ci.org/hMatoba/PlayDockerTest
comment: 0