技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

テストコードが根付かない理由を考える(2)

前回分はこっち。

white-azalea.hatenablog.jp

前回、よく問題視されてる以下3つを揚げ、そのうち「手でやった方が早い」を考えてみた。

  • 手でやった方が早い
  • 結局手動で網羅させられる
  • 品質会計とか既存の仕組みにに乗せれないじゃん?(品質基準を図れない)

そこでの観点はいくつかあって、

  • 仕組みの問題
    • 設計手法が一筆書きではテスト可能なコードは書けない
    • 一筆書きでリリースして終わりなら、手動でやった方が楽
    • 大規模にテスター集めてスケールするという手段に出るなら、Excel で管理して網羅する方が手っ取り早い
  • 開発者の練度
    • 保守でテストを繰り返す事が、多くの開発者には他人事という意識
    • テストしやすいコードを書けない(そういう設計ができない)

までコメント欄も含め見つかった。
分類は主観なので、指摘があれば突っ込んで欲しい。
問題が分かれば対処方もあるわけで、コレを元にいつかどうすべきか論をまとめたい。

他の問題がありそうならコメントで意見が欲しい。 真にやりたいのは、テストコードを書かせる為のハードルをどうにかしたいという事なのだが、ハードルが他にもあるならそれも考えるべき対象だ。

「結局手動で網羅する」を考える

今回のテーマはこれ。
やり方はなぜ何ですが、他の流れを思いつく方はコメントにGO!

このお題も、現場で良く聞きます。
経験上は二つパターンがあって、

  • 「コードにバグが出るのに、テストコードに出ないわけないだろ」
    → テストをコードですること以前に「コード自体信用できない」って言ってるタイプ
  • 「そのテストで何が証明されてるのか見た目わからないから手動でやって」 → テストの味気なさに不安を感じるタイプ

どっちもなんと言うか…って感じですね。

「コードにバグが出るのに、テストコードに出ないわけないだろ」

「そーですね」がまぁ第一応答です(苦笑

こう言われるケースは思い込みか、やりたくない言い訳かの二択です。
なぜなにも何もない…

テストをまともに書くなら、基本的には本体とテスト、両方が正常でなければOKにはなりません。
つまり、二重チェックと言えます。
これが二重のバグで運悪く成功するという確率は低いと見るべきでしょう。

  • テストレビューしたくない
  • やりたくない
  • 何が正しいかわかってなくて、テスト時の動きでそれを見極めようとした

という所でしょう。
最後の問題のケースは論外です。コレは設計ができていないという事の証拠ですので。

前者二つは論破こそできますが、実は解決策が非常に少ない問題のような気がしますね。
だって気持ちの問題ですから…

やりたくないだけの人に書かせる方法は?

このケースではふた通りを見たことがあります。

  • まじで面倒でやりたくない
  • テストコードの効果が疑問

前者はなんでしょう? 多分すぐこの話題に触れると思いますが、テストコードを書いてレビューと実行をパスすることで網羅的テストはパスしたとみなすのは当然として…
それでも書かない人には強制するしかない?

後者のテストコードの効果に疑問というのは、正常系だけでも書いてみれば否応無く理解できると思います。
となると

「正常系だけでも書くべし」というプロジェクトルールを作る

しかないのかなと…

「そのテストで何が証明されてるのか見た目わからないから手動でやって」

これマジで言われた事あるんですよ…
で、翌々考えてみると、手動テストと違う点が見えてくるんですね。

  • スクリーンショットとかが残ってないので、そのテストが正しいのか直感的に理解できない。
  • 単体テストは機能単独のテストだけど、画面からのテストは複数の機能の組み合わせ。 機能単独の動きが「こうだ」と言って、画面仕様が満たせている確証が持てない。

まずスクリーンショットの件から考えましょう。
しかしこれは、実は単体テストの事を言っていないのですよね…

多分これは前提からして間違えてて、彼らの言う単体と言うのが、「画面」とか「結合した機能」っていうプレフィックスがつくのです。
これはオブジェクト指向のテストとしては結合テストを意味しているように思います。

単体テスト - Wikipedia

ユニットとはアプリケーションのテスト可能な最小の部品単位である、と直観的にとらえることができる。手続き型プログラミングでは、ユニットは、モジュール全体のこともあるが、より一般的には、個々の関数や手続きである。オブジェクト指向プログラミングでは、ユニットは、クラスなどのインタフェース全体だが、個々のメソッドであることもある。

とはいえ、エビデンスが残らないというのは、単体テストだと正誤表しか出てこないわけですね。
それが正しいかどうかは、テストコードレビューをするしかない。
ここで品質管理者がレビューを拒否してしまった(ないしは)ら…打つ手がありません。

と、なれば単体テストを行う条件として、

品質を判定する人が、テストコードのレビューを行う事

あとは分かりやすい様に、どの機能のどの部分のテストなのかを明示しておくのが良いと思う。
普通に考えればクラス図云々の説明なのだろうけど、個人的には、マインドマップを推したい。

言ってみれば当然の話なのだけど、コレができないならコードのテストお書いても無駄になりそうですね…。
ではもう一つはどんな感じでしょう?

  • 単体テストは機能単独のテストだけど、画面からのテストは複数の機能の組み合わせ。 機能単独の動きが「こうだ」と言って、画面仕様が満たせている確証が持てない。

ここから分かるのは、実装に近いレベルの設計を管理者が把握していないケース。
コレを防ぐ方法…なんて一つですよね多分

設計方針と、設計書をレビューするしかない

至極真っ当で当たり前の話に行き着いてしまった…。
ただまぁこの当たり前を完全にこなすのが案外難しいんですって…。

プログラマ脳を鍛える数学パズル 15

10 段の階段があり、上と下から人が移動してくる。
一度に移動できるのは4段までという条件下で、同じ段に止まる手順は何通りあるか?

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

上と下の挟み撃ちではなくて、合計の移動量がジャスト 10 となるポイントを探すと読み替えた。
求められてるのが手順じゃなくて、パターン数のみなので、こうした方がメモ化のキャッシュ利用効率が上がるかなと思った。

N=10
memo = {}


def step_count(remain: int) -> int:
    if remain < 0:
        return 0
    if remain == 0:
        return 1

    if remain in memo:
        return memo[remain]

    counter = 0
    for u in range(1, 5):
        for d in range(1, 5):
            counter += step_count(remain - (u + d))

    memo[remain] = counter
    return counter

if __name__ == "__main__":
    print(step_count(N))

プログラマ脳を鍛える数学パズル 14

要するにもっとも長くしりとりが続く順番を求めよ。

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

一番時間がかかったのが、デバッグ
なんせ words のタイポとか超見つけにくい… |||orz

words = [
    'Brazil', 'Croatia', 'Mexico',
    'Cameroon', 'Spain', 'Netherlands',
    'Chile', 'Australia', 'Colombia',
    'Greece', 'Cort d\'Ivoire', 'Japan',
    'Uruguay', 'Costa Rica', 'England',
    'Italy', 'Switzerland', 'Equador',
    'France', 'Honduras', 'Argentina',
    'Bosnia and Harzegovina', 'Iran', 'Nigeria',
    'Germany', 'Portugal', 'Ghana',
    'USA', 'Belgium', 'Algeria',
    'Russia', 'Korea Republic'
]

longst_chain = []


def chain(word: str, current_chain: list, usables: list):
    global longst_chain
    current_chain.append(word)
    print(f'Start: {current_chain[0]}, List: {current_chain}')
    #print(f'  Usables: {usables}')
    #input()

    if len(current_chain) > len(longst_chain):
        longst_chain = current_chain[:]

    for next_word in usables:
        if word[-1] != next_word[0]:
            #print(f'Skip {word}, {next_word}')
            continue

        pos = usables.index(next_word)
        arg_words = usables[:]
        arg_words.pop(pos)
        chain(next_word, current_chain[:], arg_words)


if __name__ == "__main__":
    upper_words = [v.upper() for v in words]
    for word in upper_words:
        pos = upper_words.index(word)
        arg_words = upper_words[:]
        arg_words.pop(pos)
        chain(word, [], arg_words)
    print(f'Length: {len(longst_chain)}, List: {longst_chain}')

React とか Gradle とか

MacJava と Node と Gradle と React 突っ込んで連携させようとしたメモ

Homebrew 話はそれからだ

インストールは超簡単 ここ 見てコピペすればok

次はJava

最近バージョアップでゴタゴタ感のある Java だけど、Oracle に付き添う気はないので、Zulu にしたい。
が、個人的には環境も組み替えたいので、

$ brew install jenv
... なんか色々
$ jenv --version
jenv 0.4.4

忘れないうちに .bash_profile

export JENV_ROOT="$HOME/.jenv"
if [ -d "${JENV_ROOT}" ]; then
  export PATH="$JENV_ROOT/bin:$PATH"
  eval "$(jenv init -)"
fi

次に ZuliJava をダウンロード+インストール。

f:id:white-azalea:20181226203903p:plain

迷わず最新。

$ java -version
openjdk version "11.0.1" 2018-10-16 LTS
OpenJDK Runtime Environment Zulu11.2+3 (build 11.0.1+13-LTS)
OpenJDK 64-Bit Server VM Zulu11.2+3 (build 11.0.1+13-LTS, mixed mode)

そしたらコレを jenv の配下に入れる。

$ /usr/libexec/java_home -V
Matching Java Virtual Machines (1):
    11.0.1-zulu-11.2+3, x86_64:    "Zulu 11"    /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home

$ mkdir -p ~/.jenv/versions
$ jenv add /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
openjdk64-11.0.1 added
11.0.1 added
11.0 added
$ jenv global openjdk64-11.0.1
$ jenv rehash

流れで Greadle

コレは Homebrew から入る。

$ brew install gradle

これ以上何を書くことがあろうか?

nodeJS をブチ込む

これもバージョン管理したいので、nodebrew を突っ込む

$ brew install nodebrew
...いろいろ
$ vim .bash_profile
export PATH=$HOME/.nodebrew/current/bin:$PATH #←追記
$ /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

これで準備完了。

$ nodebrew ls-remote
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6    
v0.1.0    v0.1.1    v0.1.2    v0.1.3    v0.1.4    v0.1.5    v0.1.6    v0.1.7
...
v11.0.0   v11.1.0   v11.2.0   v11.3.0   v11.4.0   v11.5.0  
...
$ nodebrew install v11.5.0 
Fetching: https://nodejs.org/dist/v11.5.0/node-v11.5.0-darwin-x64.tar.gz
######################################################################## 100.0%
Installed successfully
$ nodebrew ls
v11.5.0

current: none
$ nodebrew use v11.5.0
use v11.5.0
$ node --version
v11.5.0

これも完了。

続きを読む

プログラム脳を鍛える数学パズル13

アルファベットを数字に置き換え、成立させてください。

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

import re
import itertools
import copy

src = 'READ + WRITE + TALK = SKILL'
num_expression = re.split('[^a-zA-Z]+', src)
unique_chars = list(set(''.join(num_expression)))
non_zero_chars = [n[0] for n in num_expression]


def is_invalid(kv: dict) -> bool:
    for c in non_zero_chars:
        if kv[c] == 0:
            return True
    return False


count = 0
for perm in itertools.permutations(range(10), len(unique_chars)):
    zipped = dict(zip(unique_chars, perm))

    if is_invalid(zipped):
        continue

    tmp_str = src
    for k in zipped.keys():
        tmp_str = tmp_str.replace(k, str(zipped[k]))

    if eval(tmp_str.replace('=', '==')):
        print(tmp_str)
        count += 1

print(count)

python は名前付き break がないので、中途半端に関数出し…。
ちなみに解き方は直接文字を数字に置き換えて、文字列のまま eval に食わせるという力技。

つっても結局汎用的に解こうとすると 10! ケースがあり得るのだから、もう諦めかなと。

この足し算の部分が、引き算とか、掛け算とか汎用的に…と言われてしまうと、eval 無い言語で解くの死ぬほど面倒そうな…

遺伝的アルゴリズムでナップザック問題

実際には以下の書籍の写経だけど。

機械学習と深層学習 Pythonによるシミュレーション

機械学習と深層学習 Pythonによるシミュレーション

書いたソースはコレ

github.com

平均応答を見る限り、7-11回目位の成績が良くて、その後若干劣化した。
過学習状態なのだろうかとも思うが…

毎回この手のコード見ると思うのだけど、驚くほど抽象化してない。
要するに読みづらい。

何かの折に、リファクタしたいな…

遺伝的アルゴリズムは、最適解こそ出てくる保証がないが、8-9割くらい正解に近い解答なら短時間でたたき出すことができる…らしい。
そりゃ全幅探索なんてするよりかは早いんだろうけど…全幅探索も、メモ化できる範囲ならかなり高速に解けるので、そこまで利用できるかは良くわからない。

プログラマ脳を鍛える数学パズル 12

平方を取った時…なんて問題。
問題文は本読んで…

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

文字列化して、先頭 10 桁を取り、0-9 が含まれてればOK

from math import sqrt


def contain_nums(v):
    # 0-9 が含まれてるかどうか
    for n in range(10):
        str_n = str(n)
        if not str_n in v:
            return False
    return True


num = 0
while True:
    num += 1
    sq = '{0:10.10f}'.format(sqrt(num))
    sq = sq.replace('.', '')  # 整数部分を含む
    if contain_nums(sq[0:10]):
        print(f'num:{num}, value:{sqrt(num)}')
        break

num = 0
while True:
    num += 1
    sq = '{0:10.10f}'.format(sqrt(num))
    idx = sq.index('.') + 1
    sq = sq[idx:]  # 小数点以下のみ
    if contain_nums(sq[0:10]):
        print(f'num:{num}, value:{sqrt(num)}')
        break