sbt + scalatest で副作用のあるコードをテストすると上手くいかないことがあるでござる

結論から書くと副作用があるコードが許されるのは小学生までだよねーキャハハというお話(原因見つけるまでとても大変でした;ω:)

実際やってみる

http://d.hatena.ne.jp/alpha_neet/20111012/1318437291 を参考に sbt + scalatest の環境を作る。

vim src/test/scala/test.scala

import org.scalatest.FunSuite
import org.scalatest.matchers.ShouldMatchers  

// 今回はサンプルだからここに置いてるがほんとは src/main/scala/ の中にある感じ
object O { var num = 0 }

class Test1 extends FunSuite with ShouldMatchers {
  test("1") {
    val num = 1
    O.num = num
    
    println("----------")
    println("1: start")      
    println("1: num = " + num)
    println("1: O.num = " + O.num)      
    println("----------")
    
    O.num should be (1)
  }

  test("2") {
    val num = 2
    O.num = num
      
    println("==========")
    println("2: start")
    println("2: num = " + num)
    println("2: O.num = " + O.num)
    println("==========")
    
    O.num should be (2)
  }  
}


class Test2 extends FunSuite with ShouldMatchers {
  test("3") {
    val num = 3
    O.num = num
      
    println("##########")
    println("3: start")
    println("3: num = " + num)
    println("3: O.num = " + O.num)
    println("##########")
    
    O.num should be (3)
  }
}

これテストを実行してますと下みたいな感じに失敗します。
実行するたび表示される順番は違います。

> test
##########
3: start

                  • -

1: start
3: num = 3
1: num = 1
3: O.num = 1
##########
1: O.num = 1

                  • -

==========
2: start
2: num = 2
2: O.num = 2
==========
[info] Test:
[info] - 1
[info] - 2
[info] Test2:
[info] - 3 *** FAILED ***
[info] 1 was not equal to 3 (test.scala:44)
[error] Failed: : Total 3, Failed 1, Errors 0, Passed 2, Skipped 0
[error] Failed tests:
[error] Test2

まれに奇跡的に順番が上手くいきテスト成功したりしますが、大体失敗します。

why?

見たら分かるやろ!!明かにTest1クラスとTest2クラスが別スレッドで同時に実行されてるからやないか!!あほかお前!!!


と3時間前の自分に言ってやりたいですorz
や、最初はテストコードがもっと長くてどこが原因かほんと分からなかったんです。。。
急に動かなくなったように見えたのれす。


まぁやっぱり副作用のあるコードはあかんですな。はい。

なんでそんなコード書いたの?

なんか設計考えるのめんどくさかったんです(というか能力不足でどういう設計にすればいいのか分からない)
ゲームのマップデータ(Areaって名前のクラスにしてます)があるとして、
全体で一つだし適当な object に一つ入れておくかーと思ったんです。↓みたいな感じですかね?

class Area(width: Int, height: Int) {
  // 色々ゲームのマップデータとか処理をずらずら
}
object Manager { var area: Area = null }
class Player {
  def move(add: Position) { Manager.area.check(this.pos); this.pos += add }
}

サンプルの副作用のある O.num が Manager.area に当てはまります。
これにちゃんとした値が入ってなかったせいでテストが上手く動きませんでした。

class Area(width: Int, height: Int)
class Player(area: Area)
  def move(add: Position) { area.check(this.pos); this.pos += add }
}

上記のような感じで全てのプレイヤーのインスタンスに area を持たすべきなんでしょうかね?(Area自体は全体で一つしかいらない仕様)
こうでもしないと副作用ないコードが書けないような気がするんですのです。


とか色々考えてると設計めんどくさいwwもう動けばいいかwwwと開き直ってグローバル変数もどきなのを使いまくったらテストで死んでしまいました。


なんかほんとクラス間の繋りをどうすべきかが分かんないので、良書とか何かアドバイスとかあればよろしくお願いしまつ・ω・;