ASP.NET Core MVCでブログを作ってCI構築してそこからデプロイするようにした

 .NET Core 2.0が出たので、このブログのエンジンを再構築しなおした。ついでにDockerコンテナで運用するようにし、ホスティングもコンテナでやるHyper.shに移る。
デプロイは、
開発→Github→AzureWebApp
だったのが、
開発→Github→TravisCI→Hyper.sh
になり、自動テストを経ることになった。masterへのコミットのみ、Hyper.shへのデプロイが実行されるようにしてある。あとはCIでOAuthつけたらひと段落。
https://github.com/hMatoba/tetsujin


https://blog.hmatoba.net
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

Dockerコンテナに入った.NET Core WebアプリのSeleniumでのテスト

 Visual Studio 2017から、.NET CoreのWebアプリにDockerサポートがつけられるようになった。



 これを選択してWebアプリを作成すれば、DockerコンテナでそのWebアプリを動かすための設定ファイルも自動生成される。これでWebアプリは容易にコンテナ化できる。今回はこのコンテナに入れたアプリをSeleniumでテストできるようにする。
 今回の一連のファイル準備から変更は、Githubで追えるようにしてある。
https://github.com/hMatoba/DNCinDC


 まずなにはともあれ、VisualStudio2017でWebアプリを作る。.NET Core製で、もちろんDockerサポートを有効にして作成する。ファイル一式が自動生成されたら、とりあえずデバッグを実行してみる。




 デバッグを実行するたびに、ホストにバインドされるポートの番号が変わる。これがデバッグでは面倒なことになるので、ポート番号を固定してしまう。docker-compose.ymlを編集し、ポート固定の設定を加える。



docker-compose.yml

version: '2'

services:
webapp:
image: webapp
build:
context: ./WebApp
dockerfile: Dockerfile
ports:
- "8888:80"

https://github.com/hMatoba/DNCinDC/commit/24847963b7ee5e3422d0b9f08b2cea71cd389719


 続いてSeleniumを用意する。と言ってもaptだのyumだのを使ってごにょごにょしない。Dockerコンテナで手っ取り早く用意する。
 再びdocker-compose.ymlを開き、Seleniumコンテナの用意を追記する。
version: '2'


services:
webapp:
image: webapp
build:
context: ./WebApp
dockerfile: Dockerfile
ports:
- "8888:80"
selenium:
image: selenium/standalone-chrome
ports:
- "8889:4444"
links:
- webapp

 seleniumのツリーでportsの設定によって、ホスト環境の8889ポートでseleniumがlistenするようになっている。linksは、SeleniumコンテナからWebアプリにホスト名で接続できるように書いている。こう書くことで、Seleniumコンテナから、作成したWebアプリコンテナへは、"http://webapp"でアクセスできる。
 ここでデバッグをVisuslStudioでおこない、コンテナが用意されたのち"http://127.0.0.1:8889"へブラウザでアクセスすれば、Seleniumがコンテナによって用意されたのが確認できる。

 あとはSeleniumを使ったテストを書く。C#でSeleniumを動かすのはめんどうだという経験があるので、今回はPython3.6で用意する。モジュールはunittestを使うので別途インストールはいらないが、seleniumドライバをpipあたりで用意しておく。
 ソリューション内に、Pythonプロジェクトを追加する。デフォで作成されたPythonスクリプトをリネームし、setup.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',
],
)

 続いてテストスクリプトを用意する。テストプロジェクトとして作られたPythonプロジェクトにtestsというディレクトリを作成し、そこにs_test.pyを作成する。中身は下記。
import unittest


from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

HOST = "http://webapp"

class BrowserTests(unittest.TestCase):
"""tests for main five functions."""

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

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



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


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

 これでPythonによってSeleniumをドライブさせるテストは用意できた。ここでVisualStudioのテストエクスプローラを見てみる。そこにPythonで書いたテストが表示されるはずである。ソリューションをリビルドしてもそこになにも出てこなければ、いったんVisualStudioを落として立ち上げ直す。
 テストエクスプローラにテストが表示されたら、デバッグを実行し、コンテナを動かし直す。コンテナが用意されたのを確認出来たら、テストエクスプローラにある、実行したいテストを選んで実行する。”すべて実行”を選ぶと、ソリューションのリビルドが走ってコンテナが思い通りに動作しなくなる。だからテストは部分的に実行。



https://github.com/hMatoba/DNCinDC/commit/30a1406591bc16e4469107d9c21d7e187a8ed159


 .NET CoreのWebアプリをE2Eでテストできるようにした。これでCIに進める。
comment: 0

Seleniumでブログ自動投稿

 以前にあるブログへの自動投稿システムがほしいという要望があって、それをSeleniumでやった。

 ログインのやり方などは使用例が見つかるので良かったが、WYSIWYGを使っているtextareaへの入力がちょっと手間取った。ふつうは要素を探してその内容として文字列を挿入とやっていたのだが、WYSIWYGのフォームを適用されているとどう文字列を入れればええのよと。Seleniumではキーボード入力もシミュレーションできるので、結局要素をクリックしてキーボードシミュレーションで文字列を流し込んだが。

 そういうわけであるブログで記事投稿をする、C#でSeleniumを使ったコード。やってみた感想として、Seleniumの使い道はテスト、動作確認なのでそこまで型に縛られる必要はないだろうから、Pythonとかスクリプト言語でやったほうが楽だったなというところ。

using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;

namespace PlaySelenium
{
class Program
{
private static FirefoxDriver driver;
private static LogWriter log = new LogWriter("Selenium Test Sample");

static void Main(string[] args)
{
var url = "http://*****";

driver = new FirefoxDriver();

// Go to the home page
driver.Navigate().GoToUrl(url);

driver.FindElement(By.Id("usr_name")).SendKeys("*****");
driver.FindElement(By.Id("usr_password")).SendKeys("******");
driver.FindElement(By.Name("Submit")).Click();

if (driver.Title.Contains("ログイン"))
{
driver.FindElement(By.Name("user_name")).SendKeys("******");
driver.FindElement(By.Name("password")).SendKeys("******");
driver.FindElement(By.Id("login_button")).Click();
}

driver.Navigate().GoToUrl("http://******");

driver.FindElement(By.Name("entry_title")).SendKeys("エントリータイトル");

// switch ckeditor to source mode
driver.FindElement(By.XPath("//label[@for='sourceMode']")).Click();

// wait...
driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(2));

// click to activate HTML input form
driver.FindElement(By.ClassName("CodeMirror-code")).Click();

// keyboard input emulation
driver.Keyboard.SendKeys("<h1>わっしょい</h1>");

// submit form
driver.FindElement(By.Name("srvEntryInputForm")).Submit();


log.Close();
driver.Quit();
driver.Dispose();
}


private static void AssertEqual(string a, string b)
{
if (a == b)
{
return;
}
else
{
var output = $"Test failed!\n" +
$"Page Title: {driver.Title}\n" +
$"URL: {driver.Url}\n" +
$"'AssertEqual' failed: {a} != {b}";
Console.WriteLine(output);
log.Add(output);
log.Close();
driver.Dispose();
Environment.Exit(1);
}
}

private static void AssertEqual(bool a, bool b)
{
if (a == b)
{
return;
}
else
{
var output = $"Test failed!\n" +
$"Page Title: {driver.Title}\n" +
$"URL: {driver.Url}\n" +
$"'AssertEqual' failed: {a} != {b}";
Console.WriteLine(output);
log.Add(output);
log.Close();
driver.Dispose();
Environment.Exit(1);
}
}
}
}
comment: 0