読者です 読者をやめる 読者になる 読者になる

もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

エンジニア立ち居振舞い : オンラインコミュニケーションに少しの気遣い

ぽえむ

お題「エンジニア立ち居振舞い」 ということで普段自分が気をつけてることでも書いてみます。

今でこそコミュニケーションはSlackやQiita Teamなどのツールを使い、レビューはGitHubの上でPull Requestを送ってそこにコメントするという開発スタイルになっていますが、自分のエンジニア歴の中ではここ3年くらいの出来事です。

そこで昔を振り返って今と比べてみると、開発を進める上で、オンラインでのテキストコミュニケーションが格段に増えたと思うんですよね。すぐ近くの席にいるのにSlack上で盛り上がったりするのもよくある光景になりました。

で、気づいたことがあって、テキストのコメントは不思議と受け取る人によってはキツめにとられることがあるんですよね。実体験としては、ある問題に対してチャット上ですごい厳しいことを言っている人がいて、このままだと荒れそうだから直接話に行ったら意外と揉めなくて、アレ?というような感じのことがありました。

これってすごくもったいないことだと思うんですよね。チームにJoinしたばかりの人とかはこういうシーンをチャットでみると、あの人は怖そうな人だなって印象がついてしまって、コミュニケーションを取る時に無駄に身構えてしまったりします。もちろん正しいことを正しいと伝えるのは大事ですが、伝え方次第では受け手も素直に受け取れなくて無駄なコミュニケーションコストが発生します。

なので自分としては投げるコメントには、より気を使うということを心がけています。特に大事なシーンで自分の意見を言う時に、誰かを非難するように捉えられないか、何かを否定しているように捉えられないかというのを、一呼吸おいて見返してから書き込むようにしています。

ツールが発達して手軽にコミュニケーションができるようになったからこそ、HRTを忘れずに相手のことも考えたコミュニケーションを取るようにして、チームの良い雰囲気を育てていきたいですね。

おんやど恵で開発合宿してきた日記。開発 + 温泉 is 最高

開発合宿

会社の同僚と開発合宿するぞ!という話になったので、おんやど恵さんの開発合宿プランを使って4人で行ってきました。今回は会社のイベントではなくて個人でアプリ作ったりサービス作ったりしてる人たちの集いで、各々自分の作りたいものを普段とは違うところで作るぞ!という趣旨です。

www.onyadomegumi.co.jp

事前の情報でこちらの旅館を選択した理由としては以下のポイントで選びました。

合宿の様子

  • 日程は10/29(土)〜10/31(月)にかけて予約しました。月曜は平日なので各自有給か午前半休で解散後に会社行って少し仕事するという感じです。

1日目

  • 湯河原に14:00集合です。

f:id:Rei19:20161101124244p:plain:w300

  • 駅から旅館までは歩いてもいけますが、そこそこ遠いのでタクシーで向かいました。
  • あっというまに到着

f:id:Rei19:20161101124712p:plain:w300

  • この時点で14時少し過ぎたくらいです。普通に宿に泊まる場合は一番早いチェックイン時間より前なのでまだ部屋の用意ができていないのですが、開発合宿プランの場合は開発用の会議室が使えるのでそちらに案内されます。

  • ご飯の時間や館内の利用ルールなど確認して早速開発開始

f:id:Rei19:20161101125435p:plain:w300

  • 会議室はwifiと電源タップは完備で利用料金に含まれています。このあたりはうれしいですね。広さ的には8人から10人くらいまでの規模ならゆったり使えるんじゃないでしょうか。

f:id:Rei19:20161101162557p:plain:w300

  • 別料金ですが、カップ麺やお菓子なども会議室に用意されています。

  • 18:30まで各々開発して初日の夕飯に向かいます。

f:id:Rei19:20161101130158p:plain:w300

f:id:Rei19:20161101130228p:plain:w300

f:id:Rei19:20161101130250p:plain:w300

  • 質、量ともに十分なご飯でした。むしろ食い過ぎた。。。
  • 初日は日本シリーズの6戦がやっていたので食後の休憩がてらみんなで観戦。

f:id:Rei19:20161101130707p:plain:w300

  • 各自、温泉に行ったり会議室に戻って開発を続けたり思い思いの時間を過ごします。僕は温泉に行ってから開発に戻りました。

  • 温泉の写真なかったので旅館のサイトから紹介

http://www.onyadomegumi.co.jp/image/ph_bishamon01.jpg

  • この日は夜中の2時から3時くらいまで開発して就寝。進捗は出たのでしょうか。

2日目

  • 朝食の時間が8時だったので間に合うように起床。健康的な朝食をいただきます。

f:id:Rei19:20161101161354p:plain:w300

  • この日はフルに開発に使える日なのでみんなもくもく開発。なのであまり写真がありません。。。

f:id:Rei19:20161101162811p:plain:w300

  • お昼を食べに出かけたときに見かけた猫など。野良猫が多いのかやたら猫をみかけました。

f:id:Rei19:20161101163329p:plain:w300

  • 夕飯のお肉。おいしかったです。

3日目

  • 11:00チェックアウトなので朝食をいただいた後に追い込みの開発です。

f:id:Rei19:20161101164504p:plain:w300

  • 休憩で足湯入ると気持ちよくていいですね。

  • 10時になったら各々成果発表です。

f:id:Rei19:20161101164721p:plain:w300

f:id:Rei19:20161101164738p:plain:w300

  • 4人なのでプロジェクターは使用しないで、合宿で作ったプロダクトをワイワイしながら近くで見るという感じでした。

  • メンバーの中にはリリースまで完了した人もいました。僕は完成はしなかったので動くところまでを見せるという感じ。

play.google.com

play.google.com

tecc0.com

  • 発表が終わったら後片付けをしてチェックアウトです。あっという間の3日間でした。

f:id:Rei19:20161101165255p:plain:w300

まとめ

  • 何回か開発合宿は実施していますが、今回はかなり満足度が高かったです。貸切な環境ではないもののネットの使える会議室での作業はかなり集中できました。温泉も食事も満足。
  • 費用的なところだと今回はモニタープランでの予約でしたので通常料金からの割引がきいて、かなりお得に使えました。
  • 若干、困ったこととしては以下の点
    • 会議室が少し寒めでした。会議室のエアコンが宴会場と一緒に集中管理されているので暖房が使えなく、フロントに問い合わせてヒーターを持ってきてもらいました。
    • The 温泉街という立地だったので、昼飯を食べに外に繰り出したときに選択肢が少なくて結構さまよいました。素直に出前にするか、最寄りのコンビニで調達してくればよかったなという印象です。コンビニも片道歩いて15分くらいのところにあるのでチェックインするときにある程度買い込んでいくのかいいかもです。
  • 開発合宿やオフサイトミーティングを検討中の方はぜひ選択肢の一つとして検討してみてはいかがでしょうか。

  • 今回利用したモニタープランの案内はこちらになります。

空室検索

  • 宿本体はこちら

www.onyadomegumi.co.jp

AndroidでEspresso + Spoonでキャプチャを撮りつつUIテストを走らせる

Android Test Kotlin

Android実機上でのテストにはEspressoというテスティングフレームワークを使いますが、Spoonというライブラリと組み合わせることでUIテスト実行時のキャプチャを残すことができます。導入してみたのでメモになります。

導入

  • Espressoは導入済の状態からSpoonを入れます。この記事を書いている時点でのバージョンは以下の通りです。

    • Espresso : 2.2.2
    • Spoon : 1.6.4
  • プロジェクトのbuild.gradleにプラグインを追加。

buildscript {
    dependencies {
        ...
        classpath 'com.stanfy.spoon:spoon-gradle-plugin:1.2.2'
    }
}
  • アプリケーションのbuild.gradleにライブラリを追加。プラグインも有効にしておく。
apply plugin: 'spoon'

spoon {
    // for debug output
    debug = true

    // To execute the tests device by device */
    sequential = true

    // To grant permissions to Android M >= devices */
    grantAllPermissions = true
}

dependencies {
    ...
    androidTestCompile group: 'com.squareup.spoon', name: 'spoon-client', version: '1.6.4'
}
  • これで準備はできました。

キャプチャを撮る

  • テストメソッド内でSpoon.screenshotを呼ぶことで実行時のキャプチャを残せます
    @Test
    fun testInitializedView() {

        // これでキャプチャを残せる
        Spoon.screenshot(activityRule.activity, "initial_state")

        onView(withId(R.id.fragment_initialize_edit_hatena_id))
                .perform(closeSoftKeyboard(), scrollTo())
                .check(matches(isDisplayed()))

        onView(withId(R.id.fragment_initialize_button_set_hatena_id))
                .perform(scrollTo())
                .check(matches(isDisplayed()))
                .check(matches(not(isEnabled())))

        onView(withId(R.id.fab))
                .check(matches(not(isDisplayed())))
    }
  • 実行するときはGradleのタスクとして実行します。他のテストタスクと同じようにspoonDebugのようにフレーバーをつけることも可能です。
./gradlew spoon 

実行結果

  • 実行結果は通常のEspressoの実行結果とは異なり app/build/spoon の下に格納されます。実行されると以下のように表示されます。
  • 画像にはメソッド呼び出し時のタグが振られているのでどの時点のキャプチャかわかるようになっています。

f:id:Rei19:20160910212252p:plain:w300

コード

  • こちらが個人のアプリに導入した時のプルリクになります。参考までに。

add Spoon plugin by rei-m · Pull Request #219 · rei-m/HBFav_material · GitHub

最近の個人的なAndroidの設計とかテスト周りとかまとめ

Android Kotlin
  • 最近、Androidの設計やらテストの書き方やらを試行錯誤していて、ちょっと情報が散らばってきたので個人的なまとめです。これが絶対的にイケてる!とかじゃなくて単にいろんな人のスライド読んだり、自分で試したりして今こんな感じになったというレベルのものです。
  • コードは↓で作ったアプリをベースにいじってます。。Kotlin製のアプリなのでJavaに適時読み替えてください。

rei19.hatenablog.com

設計の話

  • 設計の目標はモジュールの責務を分割して将来の変更に強くするという感じです。Androidの場合は特に適当に作るとActivityとFragmentが膨れ上がってメンテつらい作りになりがちです。で、去年に書いた記事(Androidのデザインパターンを考えてみたの続き。Kotlin対応版。 - もやもやエンジニア)とかを経て、いまのところはMVPな構成にしてModel層をDDDライクに作るようにしてます。先日のAndroidオールスターズ2でもid:kgmyshin 氏が同様の話をしてましたが、自分も近いものを目標に作ってます。

  • 雑な絵だとこんな感じです。Activity/FragmentはViewの操作に集中して、ドメインロジックはModel層に、Presenterが両者を仲介するというふいんきですね。

f:id:Rei19:20160822002625p:plain

実際に作った例

  • 起動時にIDを設定する画面(InitializeFragment)を例にとってみます。この画面は入力されたIDが有効か問い合わせて有効なら端末に保存して次の画面に行くという仕様です。

f:id:Rei19:20160821214641p:plain:w300

  • まずはPresenterとViewをつなぐInterfaceです。ViewはFragmentに実装してActionsはPresenterに実装させます。
interface InitializeContact {

    interface View {
        fun showNetworkErrorMessage()
        fun showProgress()
        fun hideProgress()
        fun displayInvalidUserIdMessage()
        fun navigateToMain()
    }

    interface Actions {
        fun onCreate(view: InitializeContact.View)
        fun onResume()
        fun onPause()
        fun onClickButtonSetId(userId: String)
    }
}
  • 次にFragmentです。ライフサイクルやユーザーの操作で発生したイベントをPresenterに伝えるのと、上で定義したInterfaceを実装してViewの操作をするメソッドを生やしてます。
  • Presenterはテスト時に差し替えできるようにDagger経由で注入できるようにしておきます。Model層の操作はここでは一切出てきません。
class InitializeFragment() : BaseFragment(),
        InitializeContact.View,
        ProgressDialogController {

    companion object {
        fun newInstance(): InitializeFragment = InitializeFragment()
    }

    @Inject
    lateinit var navigator: ActivityNavigator

    @Inject
    lateinit var presenter: InitializeContact.Actions

    override var progressDialog: ProgressDialog? = null

    private var subscription: CompositeSubscription? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        component.inject(this)
        presenter.onCreate(this)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {

        subscription = CompositeSubscription()

        val view = inflater.inflate(R.layout.fragment_initialize, container, false)

        val editId = view.findViewById(R.id.fragment_initialize_edit_hatena_id) as AppCompatEditText

        val buttonSetId = view.findViewById(R.id.fragment_initialize_button_set_hatena_id) as AppCompatButton
        buttonSetId.setOnClickListener {
            presenter.onClickButtonSetId(editId.editableText.toString())
        }

        subscription?.add(RxTextView.textChanges(editId)
                .map { v -> 0 < v.length }
                .subscribe { isEnabled -> buttonSetId.isEnabled = isEnabled })

        return view
    }

    override fun onResume() {
        super.onResume()
        presenter.onResume()
    }

    override fun onPause() {
        super.onPause()
        presenter.onPause()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        subscription?.unsubscribe()
        subscription = null
    }

    override fun showNetworkErrorMessage() {
        with(activity as AppCompatActivity) {
            val rootView = findViewById(R.id.fragment_initialize_layout_root)
            hideKeyBoard(rootView)
            showSnackbarNetworkError(rootView)
        }
    }

    override fun showProgress() {
        showProgressDialog(activity)
    }

    override fun hideProgress() {
        closeProgressDialog()
    }

    override fun displayInvalidUserIdMessage() {
        view?.findViewById(R.id.fragment_initialize_layout_hatena_id)?.let {
            it as TextInputLayout
            it.error = getString(R.string.message_error_input_user_id)
        }
    }

    override fun navigateToMain() {
        navigator.navigateToMain(activity)
        activity.finish()
    }
}
  • 最後にView層から受け取ったイベントとModel層の操作を仲介するPresenterです。Presenterが依存するModel層のモジュールはコンストラクタで受け取るようにしています。テスト時にはモックにするなりして差し替えます。
class InitializePresenter(private val userRepository: UserRepository,
                          private val userService: UserService) : InitializeContact.Actions {

    private lateinit var view: InitializeContact.View

    private var subscription: CompositeSubscription? = null

    private var isLoading = false

    override fun onCreate(view: InitializeContact.View) {

        this.view = view

        val userEntity = userRepository.resolve()
        if (userEntity.isCompleteSetting) {
            view.navigateToMain()
        }
    }

    override fun onResume() {
        subscription = CompositeSubscription()
    }

    override fun onPause() {
        subscription?.unsubscribe()
        subscription = null
    }

    override fun onClickButtonSetId(userId: String) {

        if (isLoading) return

        isLoading = true
        view.showProgress()

        subscription?.add(userService.confirmExistingUserId(userId)
                .doOnUnsubscribe {
                    isLoading = false
                    view.hideProgress()
                }
                .onBackpressureBuffer()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    onConfirmExistingUserIdSuccess(it, userId)
                }, {
                    onConfirmExistingUserIdFailure(it)
                }))
    }

    private fun onConfirmExistingUserIdSuccess(isValid: Boolean, userId: String) {
        if (isValid) {
            userRepository.store(UserEntity(userId))
            view.navigateToMain()
        } else {
            view.displayInvalidUserIdMessage()
        }
    }

    private fun onConfirmExistingUserIdFailure(e: Throwable) {
        if (e is HttpException) {
            if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
                view.displayInvalidUserIdMessage()
                return
            }
        }
        view.showNetworkErrorMessage()
    }
}
  • これでFragmentはViewの操作のにのみ集中出来るようになって見通しがよくなりました。ViewはDatabindingを使うとよりスッキリするかと思います。

テストの話

  • ここでは主にUnitTestレベルの話をします。Androidにおけるテストは単純なJUnitのテストとエミュレーターや実機を使ったテストで分かれますが、前者を指します。

Viewのテスト

  • Viewは本来はEspressoを使ってエミュレーターなり実機なりの上でテストを動かす必要がありますが、Robolectric を使うと端末を使わずにテストができます。
  • 事前にテスト用のアプリケーションクラスを作成してViewが依存するPresenterを差し替えておきます。全体のコードはGitHubを見た方が早いと思うので差し替えの部分だけ紹介。Test用のComponetを作ってファクトリメソッドをオーバーライドして差し替えます。
open class TestApp : App() {
    override fun createApplicationComponent(): ApplicationComponent {
        return DaggerTestApplicationComponent.builder()
                .applicationModule(ApplicationModule(this))
                .infraLayerModule(InfraLayerModule())
                .build()
    }
}
  • こちらが実際のRobolectricを使ったテストケースです。Configで上で作ったテスト用のアプリケーションを指定することで、テスト対象のFragmentが依存するPresenterを差し替えて、Model層の操作を行わないようにして、実装したViewのメソッドが正しく実装されているかのみを検証します。
  • 余談ですがKotlinはMockitoのwhenやらhamcrestのisやらが予約語になってるのでバッククォートで囲む必要があってちょっとめんどくさい。。。
@RunWith(RobolectricTestRunner::class)
@Config(constants = BuildConfig::class,
        application = TestApp::class,
        sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP))
class InitializeFragmentTest {

    lateinit var fragment: InitializeFragment

    private val view: View by lazy {
        fragment.view ?: throw IllegalStateException("fragment's view is Null")
    }

    private val editHatenaId: EditText
        get() = view.findViewById(R.id.fragment_initialize_edit_hatena_id) as EditText

    private val buttonSetHatenaId: Button
        get() = view.findViewById(R.id.fragment_initialize_button_set_hatena_id) as Button

    private val textInputLayoutHatenaId: TextInputLayout
        get() = view.findViewById(R.id.fragment_initialize_layout_hatena_id) as TextInputLayout

    private val snackbarTextView: TextView
        get() = fragment.activity.findViewById(android.support.design.R.id.snackbar_text) as TextView

    private fun getString(resId: Int): String {
        return fragment.getString(resId)
    }

    @Before
    fun setUp() {
        fragment = InitializeFragment.newInstance()
        SupportFragmentTestUtil.startFragment(fragment, SplashActivity::class.java)
    }

    @Test
    fun initialize() {
        assertThat(editHatenaId.visibility, `is`(View.VISIBLE))
        assertThat(buttonSetHatenaId.visibility, `is`(View.VISIBLE))
        assertThat(buttonSetHatenaId.isEnabled, `is`(false))
    }

    @Test
    fun testButtonSetHatenaIdStatus_input_id() {
        editHatenaId.setText("a")
        assertThat(buttonSetHatenaId.isEnabled, `is`(true))
    }

    @Test
    fun testButtonSetHatenaIdStatus_not_input_id() {
        editHatenaId.setText("")
        assertThat(buttonSetHatenaId.isEnabled, `is`(false))
    }

    @Test
    fun testButtonSetHatenaIdClick() {
        val presenter = mock(InitializeContact.Actions::class.java)
        doAnswer { Unit }.`when`(presenter).onClickButtonSetId("valid")
        fragment.presenter = presenter
        editHatenaId.setText("valid")
        buttonSetHatenaId.performClick()
        verify(presenter).onClickButtonSetId("valid")
    }

    @Test
    fun testShowNetworkErrorMessage() {
        fragment.showNetworkErrorMessage()
        assertThat(snackbarTextView.visibility, `is`(View.VISIBLE))
        assertThat(snackbarTextView.text.toString(), `is`(getString(R.string.message_error_network)))
    }

    @Test
    fun testDisplayInvalidUserIdMessage() {
        fragment.displayInvalidUserIdMessage()
        assertThat(textInputLayoutHatenaId.error.toString(), `is`(getString(R.string.message_error_input_user_id)))
    }

    @Test
    fun testShowHideProgress() {
        fragment.showProgress()
        assertThat(fragment.progressDialog?.isShowing, `is`(true))
        fragment.hideProgress()
        assertNull(fragment.progressDialog)
    }

    @Test
    fun testNavigateToMain() {
        val navigator = spy(fragment.navigator)
        doAnswer { Unit }.`when`(navigator).navigateToMain(fragment.activity)
        fragment.navigator = navigator
        fragment.navigateToMain()
        verify(navigator).navigateToMain(fragment.activity)
    }
}

Presenterのテスト

  • PresenterはAndroidの実装に依存しない形になっているのでピュアなJUnitのテストになります。Contextが必要な場合などはMockitoを使ってContextのMockを作ってあげればいいかなと思います。
  • setUpでモックのViewのメソッドを空実装するのと、RxAndroidのスケジューラーを使った処理がすぐ返るように設定しています。テストの検証はMockのViewのメソッドが呼ばれたかどうかで判定するようにして、その先で何が起きるかはPresenterは関心を持ちません。
  • モデル層はMockitoを使ってテスト時に必要な処理を実装しています。
@RunWith(MockitoJUnitRunner::class)
class InitializePresenterTest {

    @Mock
    lateinit var userRepository: UserRepository

    @Mock
    lateinit var userService: UserService

    @Mock
    lateinit var view: InitializeContact.View

    @Before
    fun setUp() {

        doAnswer { Unit }.`when`(view).navigateToMain()
        doAnswer { Unit }.`when`(view).showProgress()
        doAnswer { Unit }.`when`(view).hideProgress()
        doAnswer { Unit }.`when`(view).displayInvalidUserIdMessage()
        doAnswer { Unit }.`when`(view).showNetworkErrorMessage()

        RxAndroidPlugins.getInstance().reset()
        RxAndroidPlugins.getInstance().registerSchedulersHook(object : RxAndroidSchedulersHook() {
            override fun getMainThreadScheduler(): Scheduler? {
                return Schedulers.immediate()
            }
        })
    }

    @After
    fun tearDown() {
        RxAndroidPlugins.getInstance().reset()
    }

    @Test
    fun testOnCreate_initialize_not_complete_register_user() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        val presenter = InitializePresenter(userRepository, userService)

        presenter.onCreate(view)
        verify(view, never()).navigateToMain()
    }

    @Test
    fun testOnCreate_initialize_complete_register_user() {

        `when`(userRepository.resolve()).thenReturn(UserEntity("test"))

        val presenter = InitializePresenter(userRepository, userService)

        presenter.onCreate(view)
        verify(view).navigateToMain()
    }

    @Test
    fun testOnClickButtonSetId_success_check_id() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        doAnswer { Unit }.`when`(userRepository).store(UserEntity("success"))

        `when`(userService.confirmExistingUserId("success")).thenReturn(Observable.just(true))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("success")

        verify(userRepository, timeout(TimeUnit.SECONDS.toMillis(1))).store(UserEntity("success"))
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).navigateToMain()
    }

    @Test
    fun testOnClickButtonSetId_fail_check_id() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        `when`(userService.confirmExistingUserId("fail")).thenReturn(Observable.just(false))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("fail")

        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).displayInvalidUserIdMessage()
    }

    @Test
    fun testOnClickButtonSetId_fail_check_id_404() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        `when`(userService.confirmExistingUserId("fail"))
                .thenReturn(Observable.error(HttpException(Response.error<HttpException>(HttpURLConnection.HTTP_NOT_FOUND, ResponseBody.create(MediaType.parse("text/html"), "")))))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("fail")

        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).displayInvalidUserIdMessage()
    }

    @Test
    fun testOnClickButtonSetId_fail_check_id_network_error() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        `when`(userService.confirmExistingUserId("fail"))
                .thenReturn(Observable.error(HttpException(Response.error<HttpException>(HttpURLConnection.HTTP_INTERNAL_ERROR, ResponseBody.create(MediaType.parse("text/html"), "")))))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("fail")

        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showNetworkErrorMessage()
    }
}
  • これでgradleのtestタスクでUnitTestが回るようになるのでCircleCIなどの上で動かすようにしてあげれば継続的にテストを回せる仕組みができます。

おわり

  • Androidのテストまわりがよくわかってなかったので、一旦立ち止まって調べたり試したりしたことを整理するためにメモを残してみました。
  • サンプルコードをだらだらと貼ってしまいましたが、見返してみるとレイヤーを区切ったことでテストが書きやすくなったなという印象です。まだAndroid歴浅いので、間違ってるところがあるかもしれませんが、もっといいコードを書いてスピード感ある改修ができる作りを目指したいですね。

京都の一軒家借りて開発合宿に行ってきた

開発合宿 Android
  • 2泊3日で京都の一軒家借りて一休.comのエンジニア勢と開発合宿してきました。この開発合宿は東京から離れて泊まりでもくもくして最終日にアニメの聖地巡礼をして帰るという趣のイベントです。今回はちはやふるがテーマということで開発した後にかるたの聖地の近江神宮をぶらりとしてきました。
  • 今年やったことは、2年前くらいからAndroidも開発する人になったものの、テストをちゃんと書けてなかったので去年の合宿で作ったアプリの手直しとEspressoによるUIテストのやり方とか調べながらもくもくしてました。

  • 成果発表したスライドはこれ

github.com

雑感

  • 仲間内の開発合宿は日光、飛騨ときて今回は京都で開催されたのですが、前2回に比べて京都は適当に歩いていても美味そうな飯屋がごろごろあるので誘惑が多いですwとはいえ、東京から離れて非日常に身をおいて集中して開発するという目的は達成できたのでよかったです。
  • 個人の目標はCircleCIの上でだけconnectedAndroidTestがこけるという状態が解消できなかったので微妙に未達でくやしい。。。こいつを解消して合宿で作ったPRをマージしたら僕の合宿は完了という感じです。
  • 飯の写真とか泊まったところとかは他の参加メンバーの記事を見るとよいですね。お好み焼き最高に美味かったです。

rfp.hatenablog.com

blog.shibayan.jp

ちなみに前回

rei19.hatenablog.com

もくもく会でFluxを学んだ with Babel + webpack

JavaScript Babel webpack Flux

仲間内でGWもくもく会やるか!という話になったので、僕のテーマはFluxの考え方を学んで実装できるようにしようということで久しぶりにJS書きました。

やったこと

資料は Flux でぐぐると一番上に出てくる @azu_re さんのこちらのスライドを理解しながら写経 + @ するという感じ。

10分で実装するFlux

Fluxの雑な理解

  • データの流れを一方通行にして表現するアーキテクチャ
  • 登場する要素はComponent、Action、Store、EventEmitter
    • ComponentはActionをCallする + Storeを監視していてStoreの状態が変わったイベントを受け取って自分 = Viewに反映させる
    • ActionはComponentからCallされたActionをEventとして発行する
    • StoreはStateを持つ。Actionを監視していてActionのEventを受け取って自分のStateを更新して状態が変わったEventを発行する
    • EventEmitterはBus的な役割を持っていてObserverパターンの中核を担い、データの流れを繋げる。
  • よく聞くReduxはこの辺をライブラリ化したもの。

書いてみる

書いてみて思ったこと

  • データの流れに制約を持たせることで煩雑になりがちな状態の管理をしやすくできるのかなという印象を持ちました。
  • 仕組み的には新しい物はなく考え方が大事。

次の展開

  • アーキテクチャはざっくり理解したので本業のアプリ開発に展開できるようなAndroidのサンプルを書いてみる予定。クライアントサイドの開発は状態の管理をいかに制御するかがキモですね。

SwiftでiOSアプリ開発の勉強始めた 1

Swift アプリ開発 iOS

今の会社の仕事はAndroidアプリ書いてるけどiOSアプリも書けるようになりたいということでSwiftの勉強を始めました。ブログにはやったこととか覚えたこととかを適当にメモを残していこうかなーと思います。夏くらいまでにはしょぼくてもいいのでアプリを1本ストアに載せるのが目標。iOSアプリ自体はTitaniumで書いたやつは昔リリースしたけど純粋なネイティブアプリは初めてですね。

教科書

  • 特にこれだ!という理由はないけどSwiftは開発スピード早いイメージなので新し目の参考書を選択。2月の中くらいから読み始める。

やったこと

  • PlayGround上でSwiftの文法の確認。評判通り割とモダンな感じで、書いていけば慣れるかなという印象。
  • あとは写経。コードは一応上げている。

GitHub - rei-m/HelloSwift: Swift素振り用

今週までやったこと / 覚えたこと

  • StoryBoard ・・・ これにコンポーネントを配置していく。StoryBoardのコンポーネントをViewControllerのコード上にドラッグするとそのコンポーネントの参照が取れたり、イベントを設定できたりする。AndroidfindViewByIdのようなことをGUIでやっているぽい。
  • Outlet / Action ・・・ コンポーネントの参照はOutlet、イベントはActionと定義されているみたい。作成した後にOutletの名前を変えたりするとビルドエラーになるのでその前に接続を解除する必要がある。コードからStroyBoardへの接続もできる。
  • UIViewController ・・・ 名前の通りViewController。これが画面単位にあるのかな。viewDidLoad は画面の起動時に必ず呼ばれるみたいなので、Viewのイベントや値の設定などはここで行う。Androidと同じでVCに色々仕事をつめ込まないことが作る時のコツになりそう。
  • UIButton、UILabel etc etc ・・・ UIResponder - UIView が親クラスになっていて、UIViewが表示に関する機能を提供している。
  • AutoLayout ・・・ 多様なデバイスサイズと画面の向きにレイアウトをいい感じに表示させるための仕組み。コンポーネントに対して表示位置の制約(Constraints)をつける。コツをつかむまでちょっと時間がかかりそう。
  • segue ・・・ VC間の移動の仕方が定義される。StoryBoard上でVCとVCを繋げることで作成できる。移動のアニメーションの仕方はSegueで定義する。
  • UINavigationController・・・ 複数のシーンをドリルダウンするように移動する。既存のViewと組み合わせて使う。基本的にこれを使うのかなーという印象。
  • ここまでで 「Section 13-4 ナビゲーションコントローラで遷移する」まで。