きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

スプレッドシートでAセルにBセルの文言が含まれていることを確認する(Containsみたいなの)

AセルにBセルの文言が含まれていることを確認したかった時のメモ。

固定値がAセルに含まれていることを確認することは簡単でしたが、確認したい文言を可変にして確認する方法を調べるのに時間がかかりました。

環境

  • Google スプレッドシート
    • 2021/06/17時点

ゴール

  • AセルにBセルの文言が含まれていることを確認できるようにする

わかりやすく「かいしょう」という文言から「甲斐性(かいしょう)」、「衣装(いしょう)」、「石(いし)」が含まれていることを確認する。

使い方

REGEXMATCH関数 または REGEXEXTRACT関数を使います。

=REGEXMATCH(分析対象のセル, 正規表現)
# C2セル
=REGEXMATCH(A2,B2)
# D2セル
=REGEXEXTRACT(A2,B2)

実際のユースケース

スプレッドシート上で、API仕様書からレスポンスが正しく返却されていることを突合するために使用していました。私はデータがない項目はResponseJsonには含めないAPIを作成しています。

そのため、データが正しくマッピングされているかを確認する際に、APIの定義と実際のレスポンスのマッピングがずれてしまいます。結局のところ、マッピング作業自体はパワープレイしかなかったのですが、一致しているかどうかを目で追うのは大変でした。

そのため、今回の記事を利用して、行がずれているかどうかを一目で見れるようにしたかったのです。次の画像では、REGISTER_USERがレスポンスに無かったため、行がずれていることがわかります。

※ 式の変数は逆転させています。

f:id:nainaistar:20210617195401p:plain

終わりに

Javaだとcontains()で含まれているかどうかが分かります。

"かいしょう".contains("かい");

そのため、「スプレッドシート Contains」で調べたのですが、固定値でしか取得できないようなロジックしか見つかりませんでした。

この記事を公開したことで、同じような悩みがある人が救えると嬉しいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似記事

参考記事

f:id:nainaistar:20210617195307p:plain

PythonのPytestでParameterizedTestをする

PythonのPytestでもParameterizedTestをしたかったので、それを調べた時のメモ。ParameterizedTestのメリット等は特に解説しません。

環境

  • Python
    • 3.8.6
  • Pytest
    • 5.4.3

ゴール

2021年の各月の末日を求める。求める際にはParameterizedTestを使用する。

使い方

pytestをimportして、`@pytest.mark.parametrizeで変数名と変数を指定するだけです。

次の例では、monthans変数に値を指定しています。

import pytest
  
# 2021年の1月末日は31日
# 2021年の2月末日は28日
# 2021年の3月末日は31日
@pytest.mark.parametrize('month,ans', [
    (1, 31),
    (2, 28),
    (3, 31),
])
def test_end_date(month, ans):
    assert calendar.monthrange(2021, month)[1] == ans

実行結果は、テストケースの数だけ出力されます。今回は1月から3月の3ケースを用意しているので、結果は3ケース出力されます。

pipenv run tests
  
tests/test_months.py ...

エラーメッセージについて

エラーメッセージはエラーになったテストケースごとに出力されます。

import pytest
  
@pytest.mark.parametrize('month,ans', [
    (1, 31),
    (2, 29), # 2021年はうるう年ではないので29は誤り
    (3, 30), # 3月末日は30日ではないので誤り
])
def test_end_date(month, ans):
    assert calendar.monthrange(2021, month)[1] == ans

目的通りのエラーが発生します。

pipenv run tests

tests/test_months.py .FF                                                                                                   

# 詳細エラーは記載が長くなるので省略

================ short test summary info ======================
FAILED tests/test_months.py::test_end_date[2-29] - assert 28 == 29
FAILED tests/test_months.py::test_end_date[3-30] - assert 31 == 30

ソースコード

ブログでは3月までしか書いていませんが、ソースコードは12月までテストしています。

終わりに

Pythonはまだ慣れていませんが、Pytestとは少しずつ仲良くなれてきた気がします。

もしかしたら、もっと優秀なテスト方法があるかもしれませんが、できる範囲から良くしていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似

f:id:nainaistar:20210709175247p:plain

Javaで標準入力と標準出力を使ったコードをテストする

標準入力と標準出力をテストしようとすることは基本的にはありません。標準出力ではなく、Loggerに書き込まれていることを確認することが多いでしょう。

しかし、知っておいて損はありません。今回の記事では、FizzBuzzを使って標準入力と標準出力のテストを行います。

なお、新規性はありませんので、参考先の記事を見てみてください。

環境

  • Java
    • 15

仕様

  • 標準入力に1と入力したとき、標準出力には1が出力されること
  • 標準入力に2と入力したとき、標準出力には2が出力されること
  • 標準入力に3と入力したとき、標準出力にはFizzが出力されること
  • 標準出力に他の値が出力されていないこと

FizzBuzzの仕様

この記事では、FizzBuzzの仕様は以下の通りです。FizzBuzzそのもののロジックの説明はしません。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

標準入力と標準出力のテスト

標準入力と標準出力の準備

定義として、標準入力はSystem.in、標準出力はSystem.outのことをを指します。まずは、データの設定、データの取得ができるようにSystem.inSystem.outを差し替えるクラスを作成します。

差し替える標準入力

InputStreamを継承しているクラスを作る必要があります。今回はStandardInputStreamという名前で作成します。テストしやすいようにデータを設定できるinputlnメソッドを用意します。

public class StandardInputStream extends InputStream {
  private StringBuilder sb = new StringBuilder();
  private String lf = System.getProperty("line.separator");

  /**
   * 文字列を入力する。改行は自動的に行う
   *
   * @param str 入力文字列
   */
  public void inputln(String str) {
    sb.append(str).append(lf);
  }

  @Override
  public int read() {
    if (sb.length() == 0) return -1;
    int result = sb.charAt(0);
    sb.deleteCharAt(0);
    return result;
  }
}

差し替える標準出力

PrintStreamを継承しているクラスを作る必要があります。今回はStandardOutputStreamという名前で作成します。標準出力したデータを取得できるreadLineメソッドを用意します。

public class StandardOutputStream extends PrintStream {
    private BufferedReader br = new BufferedReader(new StringReader(""));

    public StandardOutputStream() {
        super(new ByteArrayOutputStream());
    }

    /**
     * 1行分の文字列を読み込む
     * @return 改行を含まない文字。終端の場合はnull
     */
    public String readLine() {
        String line = "";
        try {
            if ((line = br.readLine()) != null) return line;
            br = new BufferedReader(new StringReader(out.toString()));
            ((ByteArrayOutputStream) out).reset();
            return br.readLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

テスト対象のプロダクトコード

FizzBuzzの変換クラスと、標準入力から受け取った値をFizzBuzzクラスに渡して標準出力に渡すクラスを作成します。

public class FizzBuzz {
  public String convert(int input) {
    var sb = new StringBuilder();
    if (input % 3 == 0) sb.append("Fizz");
    if (input % 5 == 0) sb.append("Buzz");
    if (sb.length() == 0) sb.append(input);
    return sb.toString();
  }
}

public class FizzBuzzInput {
  public void main() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    try (Scanner sc = new Scanner(System.in)) {
      System.out.println(fizzBuzz.convert(sc.nextInt()));
      System.out.println(fizzBuzz.convert(sc.nextInt()));
      System.out.println(fizzBuzz.convert(sc.nextInt()));
    }
  }
}

テストコード

標準入力はSystem.setIn、標準出力はSystem.setOutで差し替えることができます。

@BeforeEachでテストを実行するたびに、先ほど作成したStandardInputStreamStandardOutputStreamを設定します。なお、Systemの値はグローバルなので、そのままに設定していると他のクラスに影響が出てしまいます。@AfterEachSystem.setInSystem.setOutに初期値を設定することを忘れないようにしましょう。

初期値については、Java15でのSystemクラスで確認しておりますが、最新版で処理を確認しておくと良いでしょう。

private StandardInputStream in = new StandardInputStream();
private StandardOutputStream out = new StandardOutputStream();

@BeforeEach
public void before() {
  System.setIn(in);
  System.setOut(out);
}

  @AfterEach
  void tearDown() throws Exception {
    System.setIn(new BufferedInputStream(new FileInputStream(FileDescriptor.in)));
    System.setOut(new PrintStream(new BufferedOutputStream(new FileOutputStream(FileDescriptor.out), 128), true, StandardCharsets.UTF_8));
  }

標準入力にデータを設定するinputlnメソッド、標準出力からデータを取得するreadLineメソッドを用意したので、そこからテストデータのセット、及びデータの検証をしましょう。今回は、標準入力に1, 2, 3を渡し、標準出力に1, 2, Fizzが取得できることを確認します。

  FizzBuzzInput target = new FizzBuzzInput();

  @Test
  public void _1and2AndFizzAndNull() {
    // Given
    in.inputln("1");
    in.inputln("2");
    in.inputln("3");
    // When
    target.main();

    // Then
    assertThat(out.readLine()).isEqualTo("1");
    assertThat(out.readLine()).isEqualTo("2");
    assertThat(out.readLine()).isEqualTo("Fizz");
    assertThat(out.readLine()).isNull();
  }

厳密なFizzBuzz問題

TDDでの演習を行う場合、変換するロジックだけをテストします。しかし、次の強調した箇所に関しては無視されることが多いです。

**1から100**までの数を**プリント**するプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」と**プリント**し、3と5両方の倍数の場合には「FizzBuzz」と**プリント**すること。

プロダクトコード

このプリントは標準出力と読み替えることができます。厳密に仕様を満たすと次のようなコードになるのではないでしょうか。

public class FizzBuzz {
  public void execute() {
    // 1から100まで実行
    for (int i = 1; i <= 100; i++) {
      // プリントする
      System.out.println(convert(i));
    }
  }
  // 変換する
  public String convert(int input) {
    // 省略
  }
}

テストコード

このFizzBuzzの厳密な定義で作ったexecuteメソッドのテストは次の観点を含むので、初心者には非常に難しいです。

  1. 標準出力のテストが難しい
  2. リターンの値が無いので容易にテストができない
  3. 本当に1-100まで出力しているか分からない
  4. 1-100まで順番に出力していない可能性があるかもしれない

しかも、何も考えずにテストしてしまうと、アサーションルーレットになってしまいます。妥協案としては、SoftAssertionsを使ってアサーションルーレットを回避するべきではないでしょうか。

  @Test
  void execute() {
    target.execute();
    int i = 0;
    SoftAssertions softly = new SoftAssertions();
    String actual = out.readLine();

    // 標準出力の値と比較
    while (actual != null) {
      softly.assertThat(target.convert(++i)).isEqualTo(actual);
      actual = out.readLine();
    }
    // 標準出力に書き込まれた回数の比較
    softly.assertThat(100).isEqualTo(i);
    softly.assertAll();
  }

備考

ポートアンドアダプターで考えると、標準入力、標準出力の機能はコアではなく、アダプターの機能です。ポートアンドアダプターについては、類似記事を参考にしてみてください。

ソースコード

終わりに

TDDでのFizzBuzzはTDDのリズムを学んでもらうもので、標準出力にこだわるべきではありません。しかし、TDDを理解するためにあえて仕様を無視をするのと、仕様を無視する癖をつけてしまうのは違います。

基本的に仕様を無視して良い理由などありませんので、伝えたいことがぼやけない様にFizzBuzzのお題を提供する側も注意しないといけませんね。


あと、私はやっていないのですが、AtCoder等の競技プログラミングでは標準入力でデータを受け取るようなので、ローカルでテストができるように標準入力の差し替えを素振りしておくといいかもしれません。


この記事お役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

類似記事

f:id:nainaistar:20210722141956p:plain