Upgrade to Pro — share decks privately, control downloads, hide ads and more …

(kotlin + patterns) * ui-tests = happy developer

Daniel
November 03, 2017

(kotlin + patterns) * ui-tests = happy developer

Talk on how Kotlin and design patterns including robots can help to improve your "test code smell" for #pataconf.com

Daniel

November 03, 2017
Tweet

More Decks by Daniel

Other Decks in Programming

Transcript

  1. (Kotlin + patterns) * ui-tests = happy developer Daniel Gomez

    Rico Android Lead Developer at Barista Ventures @aquicaipivara
  2. @Test fun test_login_showMainView() { // write email // write password

    // click login button // check if user logged in successfully } Test login works
  3. public void test_login_showMainView() throws Exception {
 selenium.open(“/main/"); selenium.waitForPageToLoad(“30000"); 
 selenium.type("name=id",

    [email protected]”);
 selenium.type("name=Password", “007"); 
 selenium.click(“name=login"); 
 selenium.waitForPageToLoad(“30000”); 
 “selenium.check(“name=main”);”
 } Login test on Selenium
  4. @Test
 fun test_login_showMainView() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"),

    ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } Login test on
  5. @Test
 fun test_login_showMainView() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"),

    ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } st
 test_login_doThis() {
 spresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 spresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 spresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 spresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 t
 test_login_withSomething() {
 presso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 presso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 presso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 presso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 presso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 presso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } @Test
 fun test_login_doMore() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSof 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard() Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSof 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard() 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(View 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.cl 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyb Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.cl 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyb 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perfor 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftK 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftK 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewAc 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click() 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } n_doOther() {
 View(ViewMatchers.withId(R.id.loginUsernameEditText))
 m(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 View(ViewMatchers.withId(R.id.loginPasswordEditText))
 m(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 View(ViewMatchers.withId(R.id.loginUsernameEditText))
 m(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 View(ViewMatchers.withId(R.id.loginPasswordEditText))
 m(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 View(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click())
 View(ViewMatchers.withText(“Main"))
 ViewAssertions.matches(ViewMatchers.isDisplayed()))
 @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard())
 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click() 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 } @Test
 fun test_login_doOther() {
 Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.close 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboar Espresso.onView(ViewMatchers.withId(R.id.loginUsernameEditText))
 .perform(ViewActions.typeText("[email protected]"), ViewActions.close 
 Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))
 .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboar 
 Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(V 
 Espresso.onView(ViewMatchers.withText(“Main"))
 .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
 }
  6. @Test fun test_login_showMainView() { // write email // write password

    // click login button // check if user logged in successfully } Login test
  7. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))

    .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard()) Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click()) Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  8. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail(“[email protected]") Espresso.onView(ViewMatchers.withId(R.id.loginPasswordEditText))

    .perform(ViewActions.typeText("007"), ViewActions.closeSoftKeyboard()) Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click()) Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  9. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("007")

    Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(click()) Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  10. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("007")

    Espresso.onView(ViewMatchers.withId(R.id.loginSubmitButton)).perform(ViewActions.click()) Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  11. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("[email protected]")

    val mainRobot = loginRobot.clickLoginButton() // Returns a MainRobot Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  12. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("[email protected]")

    val mainRobot = loginRobot.clickLoginButton() // Returns a MainRobot Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  13. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("[email protected]")

    val mainRobot = loginRobot.clickLoginButton() // Returns a MainRobot Espresso.onView(AllOf.allOf(ViewMatchers.withText("Main"))) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) }
  14. class LoginRobot { fun writeEmail(text: String) { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard()) }

    fun writePassword(text: String) { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard()) } fun clickLoginButton(): MainRobot { onView(withId(R.id.loginSubmitButton)).perform(ViewActions.click()) return MainRobot() } } class MainRobot { fun checkIsVisible() { onView(allOf(instanceOf(TextView::class.java), withParent(withId(R.id.toolbar)))) .check(matches(withText(“Main”))); } } The robots
  15. class MapApi { constructor(host: String) {} constructor(host: String, port: Int,

    listener: Listener) {} } class Order { val mapApi = MapApi("http://pataconf.com/places", 23, null) }
  16. class MapApi { object Builder { fun withHost(host: String): Builder

    { return this } fun withPort(port: Int): Builder { return this } fun withListener(listener: Listener): Builder { return this } fun build() : MapApi { return MapApi(…) } } }
  17. class MapApi { object Builder { fun withHost(host: String): Builder

    { return this } fun withPort(port: Int): Builder { return this } fun withListener(listener: Listener): Builder { return this } fun build() : MapApi { return MapApi(…) } } }
  18. class MapApi { object Builder { fun withHost(host: String): Builder

    { return this } fun withPort(port: Int): Builder { return this } fun withListener(listener: Listener): Builder { return this } fun build() : MapApi { return MapApi(…) } } } class Order { val mapApi: MapApi = MapApi.Builder.withHost("http://pataconf.com/places") .withPort(23) .build() }
  19. class MapApi { object Builder { fun withHost(host: String): Builder

    { return this } fun withPort(port: Int): Builder { return this } fun withListener(listener: Listener): Builder { return this } fun build() : MapApi { return MapApi(…) } } } class Order { val mapApi: MapApi = MapApi.Builder.withHost("http://pataconf.com/places") .withPort(23) .build() }
  20. class LoginRobot { fun writeEmail(text: String) { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard()) }

    fun writePassword(text: String) { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard()) } fun clickLoginButton(): MainRobot { onView(withId(R.id.loginSubmitButton)).perform(ViewActions.click()) return MainRobot() } }
  21. class LoginRobot { fun writeEmail(text: String): LoginRobot { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard())

    return this } fun writePassword(text: String): LoginRobot { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard()) return this } fun clickLoginButton(): MainRobot { onView(withId(R.id.loginSubmitButton)).perform(click()) return MainRobot() } }
  22. class LoginRobot { fun writeEmail(text: String): LoginRobot { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard())

    return this } fun writePassword(text: String): LoginRobot { onView(withId(R.id.loginUsernameEditText)).perform(typeText(text), closeSoftKeyboard()) return this } fun clickLoginButton(): MainRobot { onView(withId(R.id.loginSubmitButton)).perform(click()) return MainRobot() } }
  23. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("[email protected]")

    val mainRobot = loginRobot.clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } Before
  24. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("[email protected]")

    val mainRobot = loginRobot.clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } Before @Test fun test_login_showMainView() { val mainRobot = LoginRobot().writeEmail("[email protected]") .writePassword("007") .clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } After
  25. @Test fun test_login_showMainView() { val loginRobot = LoginRobot() loginRobot.writeEmail("[email protected]") loginRobot.writePassword("[email protected]")

    val mainRobot = loginRobot.clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } Before @Test fun test_login_showMainView() { val mainRobot = LoginRobot().writeEmail("[email protected]") .writePassword("007") .clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } After
  26. interface Book class PaperbackBook(val title: String, val author: String) :

    Book val book1 = PaperbackBook("Secret agents for Dummies", "James Bond") val book2 = PaperbackBook("Secret agents for Dummies", "James bond”) val book3 = PaperbackBook("Secret agents for Dummies", "James Bod")
  27. interface Book class PaperbackBook(val title: String, val author: String) :

    Book val book1 = PaperbackBook("Secret agents for Dummies", "James Bond") val book2 = PaperbackBook("Secret agents for Dummies", "James bond”) val book3 = PaperbackBook("Secret agents for Dummies", "James Bod")
  28. interface Book internal class PaperbackBook(val title: String, val author: String)

    : Book object PaperbackBookFactory { fun jamesBondBook(title: String): Book { return PaperbackBook(title, "James Bond"); } } val book = PaperbackBookFactory.jamesBondBook("Secret agents for Dummies”) val book2 = PaperbackBookFactory.jamesBondBook("Secret agents for Dummies 2”)
  29. interface Book internal class PaperbackBook(val title: String, val author: String)

    : Book object PaperbackBookFactory { fun jamesBondBook(title: String): Book { return PaperbackBook(title, "James Bond"); } } val book = PaperbackBookFactory.jamesBondBook("Secret agents for Dummies”) val book2 = PaperbackBookFactory.jamesBondBook("Secret agents for Dummies 2”)
  30. class MainTests {
 
 @Test
 fun test_onLogout_showLoginView() {
 val mainRobot

    = LoginRobot().writeEmail("[email protected]") .writePassword("007") .clickLoginButton() // Returns a MainRobot
 
 mainRobot.clickLogoutButton()
 
 loginRobot.checkIsVisible()
 } 
 } class LoginTests { @Test fun test_login_showMainView() { val mainRobot = LoginRobot().writeEmail("[email protected]") .writePassword("007") .clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } }
  31. class MainTests {
 
 @Test
 fun test_onLogout_showLoginView() {
 val mainRobot

    = LoginRobot().writeEmail("[email protected]") .writePassword("007") .clickLoginButton() // Returns a MainRobot
 
 mainRobot.clickLogoutButton()
 
 loginRobot.checkIsVisible()
 } 
 } class LoginTests { @Test fun test_login_showMainView() { val mainRobot = LoginRobot().writeEmail("[email protected]") .writePassword(“007") .clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } }
  32. class MainTests {
 
 @Test
 fun test_onLogout_showLoginView() {
 val mainRobot

    = LoginRobot().writeEmail("[email protected]") .writePassword("007") .clickLoginButton() // Returns a MainRobot
 
 mainRobot.clickLogoutButton()
 
 loginRobot.checkIsVisible()
 } 
 } class LoginTests { @Test fun test_login_showMainView() { val mainRobot = LoginRobot().writeEmail("[email protected]") .writePassword(“007") .clickLoginButton() // Returns a MainRobot mainRobot.checkIsVisible() } }
  33. class MainTests {
 
 @Test
 fun test_onLogout_showLoginView() = loginSuccessfully {

    val loginRobot = clickLogoutButton()
 
 loginRobot.checkIsVisible()
 } 
 } class LoginTests { @Test fun test_login_showMainView() = loginSuccessfully { checkIsVisible() } }
  34. open class BaseRobot { fun click(id: Int) { onView(withId(id)).perform(ViewActions.click()) }

    } class LoginRobot : BaseRobot() { fun clickLogin() { click(R.id.loginButton) } }
  35. open class BaseRobot { fun click(id: Int) { takeScreenshot("before_click") onView(withId(id)).perform(ViewActions.click())

    takeScreenshot(“after_click”) } fun takeScreenshot(screenName: String) { //... } } class LoginRobot : BaseRobot() { fun clickLogin() { click(R.id.loginButton) } }
  36. - Speak Sample: https://github.com/caipivara/androidmeetup-ui-robots - Realm / Jake Wharton: https://academy.realm.io/posts/kau-jake-wharton-testing-robots/

    - Page Object: https://martinfowler.com/bliki/PageObject.html - Android Dialogs (Sam Edwards: Espresso Robots + Screenshots): https:// www.youtube.com/watch?v=vsrX0gGZp3o - Icons: https://www.iconfinder.com/icons/58995/lionel_preacherbot_robot_icon References