Download - What makes Geb groovy?
質問1
Gebの利用経験
1. 知らなかった2. 知ってたけど「げぶ」って読むと思ってた3. インストールして、試しに動かしてみたことがある4. 仕事や趣味のプロジェクトで使っている5. むしろ作っている
自己紹介
● @PoohSunny● 現在はカエルのマークのSaasスタートアップ
チームで奮闘中● Groovy, Grails, Gradle, Geb を使ってお仕事● さっきの質問への回答
○ Gebは仕事で使っています○ 今はGroovy, 前はJava○ Gebの前はWebDriver - Javaを使ってました
Geb (「じぇぶ」と発音します)
めっちゃgroovyなブラウザ操作自動化... WEB(のGUI)テスト、スクリーンキャプチャ、and more
※ groovyには「すばらしい」とか「イケてる」といった意味があります。
http://www.thefreedictionary.com/groovy
● http://www.gebish.org/● Selenium WebDriverのラッパー● Groovy製● 2015年1月時点でのバージョンは0.10.0● ライセンスはApache License Version 2.0
今日触れること
● Gebひとまわり(これが大半)○ Gebの良いところ洗いざらい
● はじめてみよう!○ とりあえずさくっと始めてみるには
質問はいつでもどうぞ!!
もしくは #gebjpつけて @PoohSunny にメンションください!
Geb’s Highlights(Gebのページより)
● Cross Browser● jQuery-like API● Page Objects● Asynchronous Pages● Testing● Build Integration
Geb’s Highlights(Gebのページより)
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)
最初にこの2つから
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)
サンプルコード(WebDriver - Java)import org.junit.*; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.WebDriverWait; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.CoreMatchers.*; /** * http://docs.seleniumhq.org/docs/03_webdriver.jsp#introducing-the-selenium-webdriver-api-by-example * をベースに一部改変 */ public class sampleTest { WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { // Googleのページへ遷移 driver.get("http://www.google.com"); // 他のやり方として、以下のように記述することもできます // driver.navigate().to("http://www.google.com"); // nameからtext inputのエレメントを探す WebElement element = driver.findElement(By.name("q")); // 検索するために何か入力 element.sendKeys("Cheese!"); // フォームをサブミット。WebDriverはエレメントからフォームを見つけてくれます。 element.submit(); // ページのタイトルをチェック assertThat(driver.getTitle(), is("Google")); // Google検索はJavaScriptで動的に表示されます // ページがロードされるのを待ちます。タイムアウトは10秒 (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); // "Cheese!"から始まる文字が表示されているべき assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { // ブラウザを閉じる driver.quit(); } }
小さすぎて読めないので
● 基本省略します○ インポート○ クラス○ コメント
● 今日のコードの半分くらいはhttps://github.com/PoohSunny/geb-study に上げてあります。
サンプルコード(WebDriver - Java) WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
下準備
import geb.junit4.GebReportingTest;
@RunWith(JUnit4)public class sampleTest extends GebReportingTest {
これで、JUnit4でGebが利用できます。(最初はJUnit4で実行
します。)
WebDriver to Geb WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
new xxDriver() WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
driverの記述は設定ファイル(GebConfig.groovy)内で
new xxDriver() WebDriver driver; @Before public void setUp() { } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
driver.quit() WebDriver driver; @Before public void setUp() { } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
quit()はGebが呼びます
driver.quit() WebDriver driver; @Before public void setUp() { } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { }
変数定義
WebDriver driver; @Before public void setUp() { } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { }
変数定義も不要
変数定義
@Before public void setUp() { } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { }
@Before, @After @Before public void setUp() { } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { }
初期処理が不要に
終了処理が不要に
@Before, @After @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
WebDriver to Geb @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
スペースができたのでコードを広げましょう
WebDriver to Geb
@Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
WebDriver to Geb
@Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); assertThat(driver.getTitle(), is("Google"));
element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
driver.get()
@Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); assertThat(driver.getTitle(), is("Google"));
element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
リクエストの発行はgo メソッドで
go()
@Test public void googleSuggestTest() { go("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); assertThat(driver.getTitle(), is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
リクエストの発行はgo メソッドで
driver.findElement()
@Test public void googleSuggestTest() { go("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); assertThat(driver.getTitle(), is("Google"));
element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
jQueryっぽいAPIで書き換えましょう
$()
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $(input, name: “q”); element.sendKeys("Cheese!"); assertThat(driver.getTitle(), is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
jQueryっぽいAPIで書き換えましょう
jQuery的なAPI
● 正式にはNavigator APIって名前● ふじさわさんの発表で詳しく!● チートシート(日本語)
○ http://qiita.com/itagakishintaro/items/1fa06904bd0a6de73ee2
参考:http://www.gebish.org/content
element.sendKeys()
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element.sendKeys("Cheese!"); assertThat(driver.getTitle(), is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
sendKeysも書き換え
element.value()
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element.value("Cheese!"); assertThat(driver.getTitle(), is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
sendKeysも書き換え
<<
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(driver.getTitle(), is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
さらにこんな風にも書けます
driver.getTitle()
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(driver.getTitle(), is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
driverの記述は不要
title
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
driverの記述は不要
Keys
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element.submit(); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
Seleniumのキーストロークもいけます
Keys
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
Seleniumのキーストロークもいけます
WebDriverWait()
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); }
waitは冗長になってしまいがち...
waitFor()
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(driver.getTitle(), startsWith("Cheese!")); }
Gebならスッキリ
WebDriver to Geb
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(driver.getTitle(), startsWith("Cheese!")); }
driverの記述は不要(2回目)
WebDriver to Geb
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
driverの記述は不要(2回目)
WebDriver to Geb
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
余白を詰めてみます。
WebDriver to Geb
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
これが
WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
こう
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
● JVM向けの動的型付き言語● ほとんどゼロの学習曲線で現代的なプログラミン
グ機能をJava開発者が利用できるようになる● 構文がコンパクトなので、読みやすくメンテナン
スしやすいコードになる● http://groovy-lang.org/● Groovyを一回りしたい人はこちら
○ https://speakerdeck.com/glaforge/what-makes-groovy-groovy
public
@Test public void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
public の記述不要
public
@Test void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
セミコロン
@Test void googleSuggestTest() { go("http://www.google.com");
def element = $("input", name: "q"); element << "Cheese!"; assertThat(title, is("Google")); element << Keys.ENTER; waitFor { title.toLowerCase().startsWith("cheese!"); } assertThat(title, startsWith("Cheese!")); }
セミコロン不要
セミコロン
@Test void googleSuggestTest() { go("http://www.google.com")
def element = $("input", name: "q") element << "Cheese!" assertThat(title, is("Google")) element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assertThat(title, startsWith("Cheese!")) }
()
@Test void googleSuggestTest() { go("http://www.google.com")
def element = $("input", name: "q") element << "Cheese!" assertThat(title, is("Google")) element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assertThat(title, startsWith("Cheese!")) }
パラメータがあれば() 不要
()
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assertThat(title, is("Google")) element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assertThat(title, startsWith("Cheese!")) }
assert
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assertThat(title, is("Google")) element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assertThat(title, startsWith("Cheese!")) }
JUnitのassertThatもassertに書き換えてしまいましょう!
assert
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
短くなっただけでなく、別なメリットも
というわけで、これが
WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
こう
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
これが
WebDriver driver; @Before public void setUp() { driver = new FirefoxDriver(); } @Test public void googleSuggestTest() { driver.get("http://www.google.com");
WebElement element = driver.findElement(By.name("q")); element.sendKeys("Cheese!"); element.submit(); assertThat(driver.getTitle(), is("Google")); (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.getTitle().toLowerCase().startsWith("cheese!"); } }); assertThat(driver.getTitle(), startsWith("Cheese!")); } @After public void tearDown() { driver.quit(); }
こう
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
スクリーンキャプチャの取得
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER
report("検索ページ") waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
report を呼び出すと、画面のHTML, およびキャプチャを取得。
参考:http://qiita.com/nyasba/items/edf102578bde7edf0d4f
スクリーンキャプチャの取得
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER
report("検索ページ") waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
各テストケース終了時のキャプチャは、明示的に書かなくても自動取得
参考:http://qiita.com/nyasba/items/edf102578bde7edf0d4f
ここまでの話
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)● Screen Scraping(スクリーンキャプチャ)
次の話
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)● Screen Scraping(スクリーンキャプチャ)
ところで、このコード
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
もはやJUnitで動かすいみがほとんどない
Gebのページを読むと
GebはSpock, JUnit, TestNG, さらには(Cuke4Dukeを用いての)Cucumber といった、有名なテスティングフレームワークへの統合モジュールを提供しています。
参考:http://www.gebish.org/testing
Gebのページを読むと
GebはSpock, JUnit, TestNG, さらには(Cuke4Dukeを用いての)Cucumber といった、有名なテスティングフレームワークへの統合モジュールを提供しています。
参考:http://www.gebish.org/testing
I’m Spock
http://en.wikipedia.org/wiki/File:Leonard_Nimoy_as_Spock_1967.jpg
お待たせしました!
Spockで書くと
def "Googleで「Cheese!」を検索する"() { given: 'Googleを表示する' go "http://www.google.com" when: '検索欄にCheese!と入力して def element = $("input", name: "q") element << "Cheese!" and: '検索を実行する' element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } then: '検索結果のページを表示する"' title.startsWith("Cheese!") }
テストメソッド名がString(句読点とか入れられる)
Spockで書くと
def "Googleで「Cheese!」を検索する"() { given: 'Googleを表示する' go "http://www.google.com" when: '検索欄にCheese!と入力して def element = $("input", name: "q") element << "Cheese!" and: '検索を実行する' element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } then: '検索結果のページを表示する' title.startsWith("Cheese!") }
ブロックをつかってフェーズを表現できる
参考:https://code.google.com/p/spock/wiki/SpockBasics#Blocks
Spockで書くと
def "Googleで「Cheese!」を検索する"() { given: 'Googleを表示する' go "http://www.google.com" when: '検索欄にCheese!と入力して' def element = $("input", name: "q") element << "Cheese!" and: '検索を実行する' element << Keys.ENTER waitFor { title.toLowerCase().startsWith("cheese!") } then: '検索結果のページを表示する' title.startsWith("Cheese!") }
そのブロック内で何をしたいのかも書けます
参考:https://code.google.com/p/spock/wiki/SpockBasics#Blocks
whereをつかってデータ駆動テスト
@Unroll def " '#loginId' , '#password' でログインしようとすると '#error' と表示する"() { when: 'ログインしようとすると' $("#loginId").value(loginId) $("#password").value(password) $("#login-btn").click() then: 'エラーメッセージが表示される' $("#error-msg").text() == error
where: loginId | password || error "" | "aaa" || "ログインIDを入力してください。" "001 | "" || "パスワードを入力してください。" "001 | "aaa" || "該当するユーザーが見つかりませんでした。" }
http://www.slideshare.net/hirokotamagawa/20141018-2selenium-40423299 をベースにしています
whereをつかってデータ駆動テスト
@Unroll def " '#loginId' , '#password' でログインしようとすると '#error' と表示する"() { when: 'ログインしようとすると' $("#loginId").value(loginId) $("#password").value(password) $("#login-btn").click() then: 'エラーメッセージが表示される' $("#error-msg").text() == error
where: loginId | password || error "" | "aaa" || "ログインIDを入力してください。" "001 | "" || "パスワードを入力してください。" "001 | "aaa" || "該当するユーザーが見つかりませんでした。" }
http://www.slideshare.net/hirokotamagawa/20141018-2selenium-40423299 をベースにしています
テーブル記法でパラメーターを定義できる
whereをつかってデータ駆動テスト
@Unroll def " '#loginId' , '#password' でログインしようとすると '#error' と表示する"() { when: 'ログインしようとすると' $("#loginId").value(loginId) $("#password").value(password) $("#login-btn").click() then: 'エラーメッセージが表示される' $("#error-msg").text() == error
where: loginId | password || error "" | "aaa" || "ログインIDを入力してください。" "001" | "" || "パスワードを入力してください。" "001" | "aaa" || "該当するユーザーが見つかりませんでした。" }
http://www.slideshare.net/hirokotamagawa/20141018-2selenium-40423299 をベースにしています
たとえば3つめのデータでテストが失敗すると、「'001', 'aaa' でログインしようとすると '該当するユーザーが見つかりませんでした。' と表示する」と補完してくれます。
ここまでの話
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)● Screen Scraping(スクリーンキャプチャ)
次の話
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)● Screen Scraping(スクリーンキャプチャ)
クロスブラウザの自動化
● サポート○ Firefox○ Internet Explorer○ Google Chrome○ Opera○ Remote Browsers○ Headless Browsers
■ HTMLUnit■ PhantomJS
● 実験的にサポート○ Chrome on Android○ Safari on iPhone & iPad
設定も簡単
environments { chrome { driver = { new ChromeDriver() } } firefox { driver = { new FirefoxDriver() } } phantomJs { driver = { new PhantomJSDriver() } } }
※システムプロパティに ”geb.env”という名前でパラメータをセットする
たとえばgradleで
ext { // driverの指定
drivers = ["firefox", "chrome", "phantomJs"]}
drivers.each { driver -> task "${driver}Test"(type: Test) { systemProperty "geb.build.reportsDir", reporting.file("$name/geb") systemProperty "geb.env", driver } }
chromeTest { // 省略
}
最後の話
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)● Screen Scraping(スクリーンキャプチャ)
ページオブジェクト
● Selenium界隈で広く利用されているデザインパターン
● テスト対象のページをオブジェクトとしてテストから切り離し、メンテナンス性を高める
● 参考URL○ http://codezine.jp/article/detail/7527○ http://garbagetown.hatenablog.
com/entry/2013/11/07/075011
ページオブジェクトパターンの要約
● public メソッドは、そのページが提供するサービスを表現します
● ページの内部を露出しないようにします● 一般的に、アサーションは行いません● メソッドは別の PageObjects を返します● ページのすべてを表現する必要はありません● 同じアクションの異なる結果は、異なるメソッドと
してモデル化されます
※http://garbagetown.hatenablog.com/entry/2013/11/07/075011から引用
ページオブジェクトを標準サポート
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
Page
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
Pageクラスを継承
url
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
to SignInPage もしくはvia SignInPageと記述するとこのURLに遷移
at
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
at SignInPageと記述するとチェックが走る
ページ遷移にまつわるあれこれ
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
このあたりは後でもう少し詳しく触れます
content
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
ページ内のコンテンツはcontent ブロック内に
contentのwait
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
(wait: xx)と書くとコンテンツ取得時にwaitしてくれます。
module
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
複数ページで共通するコンポーネントは moduleとしてまとめられる
content内に処理
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
処理も書けます
to: xxPage
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
処理を呼び出した後の遷移先を指定
toWait: xx
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() } } }
ページ遷移が完了するまで待ってくれる
Pageを利用するテストケース
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
to
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
SingInPageへ遷移 &ページオブジェクトを移動
at
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
to でページ遷移すると at チェックがされるので、この例では不要
contentの呼び出し
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
SingInPage 内の処理の呼び出し
ページ遷移
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
この処理内で MainPageへの遷移 + ページオブジェクトの変更
at
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
次のページの at チェックを実行
別な書き方
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" SignInPage signInPage = browser.to SignInPage browser.at SignInPage and: "ユーザー名とパスワードを入力してログインすると" MainPage mainPage = signInPage.signIn "SAMPLE_USER" // 略 then: "メインページを表示する" browser.at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
参考:http://www.slideshare.net/hirokotamagawa/20141018-2selenium-40423299 http://www.gebish.org/manual /current/ide-and-typing.html#strong_typing
ページを明示的に記述
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" SignInPage signInPage = browser.to SignInPage browser.at SignInPage and: "ユーザー名とパスワードを入力してログインすると" MainPage mainPage = signInPage.signIn "SAMPLE_USER" // 略 then: "メインページを表示する" browser.at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
省略をやめる(Page内も書き換え必要)
参考:http://www.slideshare.net/hirokotamagawa/20141018-2selenium-40423299 http://www.gebish.org/manual /current/ide-and-typing.html#strong_typing
IDEで補完が効く
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" SignInPage signInPage = browser.to SignInPage browser.at SignInPage and: "ユーザー名とパスワードを入力してログインすると" MainPage mainPage = signInPage.signIn "SAMPLE_USER" // 略 then: "メインページを表示する" browser.at MainPage when: "ページ内のダッシュボードを確認すると" // ...略 }
IDEでメソッド名などの補完が効く
参考:http://www.slideshare.net/hirokotamagawa/20141018-2selenium-40423299 http://www.gebish.org/manual /current/ide-and-typing.html#strong_typing
go()
class GoogleSpec extends GebSpec { def "goメソッドはページオブジェクトのセットをしない"() { given: Page oldPage = page when: go "http://google.com" then: oldPage == page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
go()
class GoogleSpec extends GebSpec { def "goメソッドはページオブジェクトのセットをしない"() { given: Page oldPage = page when: go "http://google.com" then: oldPage == page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
元々oldPageがセットされていて
go()
class GoogleSpec extends GebSpec { def "goメソッドはページオブジェクトのセットをしない"() { given: Page oldPage = page when: go "http://google.com" then: oldPage == page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
goメソッドを呼んでも
go()
class GoogleSpec extends GebSpec { def "goメソッドはページオブジェクトのセットをしない"() { given: Page oldPage = page when: go "http://google.com" then: oldPage == page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
ページオブジェクトはoldPageのまま
go()
class GoogleSpec extends GebSpec { def "goメソッドはページオブジェクトのセットをしない"() { given: Page oldPage = page when: go "http://google.com" then: oldPage == page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
でもcurrentUrlは変わる
to() or via()
class GoogleSpec extends GebSpec { def "toメソッドはページオブジェクトをセットし、現在のurlも変更"() { given: Page oldPage = page when: to GoogleHomePage then: oldPage != page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
to() or via()
class GoogleSpec extends GebSpec { def "toメソッドはページオブジェクトをセットし、現在のurlも変更"() { given: Page oldPage = page when: to GoogleHomePage then: oldPage != page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
元々oldPageがセットされている
to() or via()
class GoogleSpec extends GebSpec { def "toメソッドはページオブジェクトをセットし、現在のurlも変更"() { given: Page oldPage = page when: to GoogleHomePage then: oldPage != page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
toメソッドを呼び出すと
to() or via()
class GoogleSpec extends GebSpec { def "toメソッドはページオブジェクトをセットし、現在のurlも変更"() { given: Page oldPage = page when: to GoogleHomePage then: oldPage != page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
ページオブジェクトが変わる(GoogleHomePageがセットされる)
to() or via()
class GoogleSpec extends GebSpec { def "toメソッドはページオブジェクトをセットし、現在のurlも変更"() { given: Page oldPage = page when: to GoogleHomePage then: oldPage != page driver.currentUrl == "http://google.com" } }
http://www.gebish.org/manual/current/all.html#directをベースにしています
currentUrlも変わる
to()とvia()の違い
● メソッド呼び出し後に atチェックが走るか○ to() => 走る○ via() => 走らない
■ 遷移先ページでリダイレクトがあるような場合はvia()を使う
● to() か、at チェックを組み合わせた via() をいつも使うのが良いアイデア
参考:http://www.gebish.org/manual/current/all.html#at_checking
とりあえずさくっと体験してみたい方
● オススメ体験法○ https://github.com/geb/geb-example-gradle○ cloneして、gradlew コマンドで実行○ たとえばmacなら、./gradlew chromeTest で実行○ こちらもApache Licence Version 2.0
■ https://github.com/geb 配下のプロジェクトは全
てApache Licence Version 2.0だそうです。
● 参考:http://markmail.org/search/?q=list%3Aorg.codehaus.
geb.user#query:list%3Aorg.codehaus.geb.user+page:1+mid:dyp256xnjaku7guq+state:results
クロスブラウザの自動化
● サポート○ Firefox○ Internet Explorer○ Google Chrome○ Opera○ Remote Browsers○ Headless Browsers
■ HTMLUnit■ PhantomJS
● 実験的にサポート○ Chrome on Android○ Safari on iPhone & iPad
ページオブジェクトのサポート
class SignInPage extends Page {
static url = "http://www.application.com/" static at = { title == "Please sign in." } static content = { signInButton(wait: true) { $("button", 0, class: "btn btn-primary") } errorMessageBox { $("div", 0, class: "alert alert-warning")} header { module Header } signIn(to: MainPage, toWait: true) { username, password -> $("#username").value(username) $("#password").value(password) signInButton.click() }
class WhenCheckNotifyOnTheDashborad extends GebSpec { def "ログインしてメインページで3件の新着通知を確認する"() { when: "サインインページを表示して" to SignInPage at SignInPage and: "ユーザー名とパスワードを入力してログインすると" signIn "SAMPLE_USER", "SAMPLE_PASSWORD"
then: "メインページを表示する" at MainPage
テスティング
GebはSpock, JUnit, TestNG, さらには(Cuke4Dukeを用いての)Cucumber といった、有名なテスティングフレームワークへの統合モジュールを提供しています。
参考:http://www.gebish.org/testing
スクリーンキャプチャ
@Test void googleSuggestTest() { go "http://www.google.com"
def element = $("input", name: "q") element << "Cheese!" assert title == "Google" element << Keys.ENTER
report("検索ページ") waitFor { title.toLowerCase().startsWith("cheese!") } assert title.startsWith("Cheese!") }
report を呼び出すと、画面のHTML, およびキャプチャを取得。
What makes Geb groovy?
● Cross Browser(クロスブラウザ)● jQuery-like API(jQuery的なAPI)● Page Objects(ページオブジェクト)● Asynchronous Pages(非同期ページの対応)● Testing(テスティング)● Build Integration(ビルドツールとの統合)● Screen Scraping(スクリーンキャプチャ)
いますぐ体験
● https://github.com/geb/geb-example-gradle
● cloneして、gradlew コマンドで実行
今日の話
基本ここに書いてある
● GebのWebページ○ http://www.gebish.org/
● The Book Of Geb○ http://www.gebish.org/manual/current/
Special Thanks (敬称略)
@cocoatomo, @ffggss, @grimrose, @ito_nozomi, @kyon_mm, @nkns165, @nobusue, @oota_ken, @setoazusa, @syobochim, たけしふ, 嫁