きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Rubyでリフレクションしてメソッドを呼び出す(public_send)

Javaだと面倒だったリフレクションがRubyだと簡単にできてしまったので、メモします。

もっと詳しく書いている記事はありますので、私の記事はメモレベルです。

環境

  • GitHub Codespaces
  • Ruby
    • 3.0
  • RSpec

対応

準備

適当なクラスを用意します。今回の記事では、Reflectionクラスを自作します。

class Reflection
    def house(name: "")
        name == "" ? "house" : format("%s house", name)
    end

    def mouse(sum: 0)
        sum == 0 ? "mouse" : format("%s mouse", sum)
    end

    def nose()
        "nose" 
    end
end

使用方法

次の使い方をします。

# 使い方
# 第一パラメータはメソッド名(文字列でもSymbolでも可)
# 第二パラメータはメソッドに渡すパラメータ
obj.public_send(name, *args)

# 実際の使い方
Reflection.new.public_send("house")
Reflection.new.public_send(:house, "kirimaru")

発生しうるエラーは次のとおりです。

  • ArgumentError
    • リフレクションで指定したパラメータのメソッドが存在しない
  • NoMethodError
    • 指定したメソッド名が存在しない

テストコード

RSpecで動作確認したときのコードです。

require 'rails_helper'

RSpec.describe 'リフレクションクラスの確認', type: :model do
  let(:reflection) {Reflection.new}

  describe 'リフレクションの確認' do
    it "ArgumentError" do
        expect do
            reflection.public_send()
        end.to raise_error(ArgumentError)
    end 

    context 'パラメータなし' do
        it "houseを呼び出す" do
            expect(reflection.public_send("house")).to eq "house"
        end 

        it "mouseを呼び出す" do
            expect(reflection.public_send("mouse")).to eq "mouse"
        end 

        it "noseを呼び出す" do
            expect(reflection.public_send("nose")).to eq "nose"
        end 

        it "gorillaを呼び出す" do
            expect do
                reflection.public_send("gorilla")
            end.to raise_error(NoMethodError)
        end 
    end

    context 'パラメータあり' do
        it "houseを呼び出す" do
            expect(reflection.public_send(:house, name: "baki")).to eq "baki house"
        end 

        it "mouseを呼び出す" do
            expect(reflection.public_send("mouse", sum: 123)).to eq "123 mouse"
        end 

        it "noseを呼び出す" do
            expect do
                reflection.public_send("nose", name: "aaa")
            end.to raise_error(ArgumentError)
        end 

        it "gorillaを呼び出す" do
            expect do
                reflection.public_send("gorilla", name: "aaa")
            end.to raise_error(NoMethodError)
        end 
    end
  end
end    

ソースコード

終わりに

私は普段Java触っているので、Rubyはまだまだ駆け出しレベルです。

Javaだとリフレクションを使用するのに少々手間かかりましたが、Rubyだと簡単に使えてびっくりしました。動的型付き言語だとリフレクション機能とか容易に使えるようになっているんですかね。

まだまだRuby, Railsを本業レベルには持っていけていないので、知識をつけていきたいです。

今のところ、Rubyの感覚としてはギリギリまで制約を緩くしている言語で、単純に作る分には楽だが、保守すると大変な言語だという認識でいます。


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

参考情報

類似情報

f:id:nainaistar:20211026120519p:plain

WindowsでGitからチェックアウトするとCRLFになって不具合発生した

副業先でdocker-compose up -dをすると、なぜか次のエラーが発生しました。

standard_init_linux.go:228: exec user process caused: no such file or directory
ERROR: 1

原因としては、DockerfileのENTRYPOINTであったシェルの改行コードがCRLFになっていたせいで、指定していたexec "$@"をうまく展開できなかったことが原因です。

今回の記事では、同様の事象が起こらないようにブログの読者に注意喚起する記事です。

環境

  • Windows

対処

Gitからチェックアウトする際に改行コードを変更しないようにします。次の設定をすることで、チェックアウト時に改行コードを変更せずに、プッシュ時にCRLFからLFに変更します。

git config --global core.autocrlf input

詳しい挙動は次の記事を参考にしてください。

終わりに

おそらく、Windows上で環境構築している場合はCRLFでも問題ないと思われます。

しかし、ここ最近ではDockerを使用した環境構築が流行っています。必然的にLinux環境上で動かすことになりますので、改行コードがCRLFだと適切な挙動が期待できません。Windowsユーザで私の記事を見た人は、すぐにGitの改行コードの設定を変更しましょう。

本来ならWindowsで開発する人はもっと早くハマる内容だったんでしょうね…。知る機会を得られてよかったです。

ちなみに、この原因にたどり着くまでに5時間消化しましたorz


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

参考情報

f:id:nainaistar:20211021231323p:plain

SpringのTestExecutionListenerのTestContextから設定ファイルの値を読み込む

小ネタ。

環境

  • Java
    • 16
  • SpringBoot
    • 2.4.5

概要

設定ファイルの値を取得したい場合は、TestContextからApplicationContextEnvironmentを経由してgetProperty(String key)で取得できます。

testContext
  .getApplicationContext()
  .getEnvironment()
  .getProperty("spring.datasource.driver-class-name")

ゴール

SpringのTestExecutionListenerのパラメータのTestContextから設定ファイルの値を読み込みます。

ユースケース

今回の記事では、SpringのJDBCドライバのクラス名がoracle.jdbc.OracleDriverである時だけ、テストメソッドを実行するアノテーションを作成します。設定するプロパティ名はspring.datasource.driver-class-nameとします。

特定の条件を満たしたときだけテストを実行する、アノテーションを作成する等は次の記事を参考にしてください。

対応

アノテーションを作成する

アノテーションを作成します。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface OracleOnly {
}

テストのリスナを作成する

アノテーションに挙動を付与します。

詳しいことは以前の記事を参考にしてください。

設定ファイルの値を取得したい場合は、TestContextからApplicationContextEnvironmentを経由してgetProperty(String key)で取得できます。

今回はユースケースを満たせるように、プロパティのspring.datasource.driver-class-nameoracle.jdb.OracleDriver以外の場合は実行しないようにします。

public class OracleOnlyTestExecutionListener implements TestExecutionListener {
  @Override
  public void beforeTestMethod(TestContext testContext) {
    // テストメソッドのアノテーションを読み込む
    var annotation = AnnotationUtils.findAnnotation(testContext.getTestMethod(), OracleOnly.class);
    if (annotation == null) {
      return ;
    }

    String property = testContext.getApplicationContext().getEnvironment()
        .getProperty("spring.datasource.driver-class-name");
    
    if (!"oracle.jdbc.OracleDriver".equals(property)){
      assumeTrue(false);
    }
  }
}

アノテーションのリスナを単体テストする

Nestedしてもうまく動かないので、JDBCドライバをOracleに設定しているテストクラスと、PostgreSQLに設定しているクラスの2クラス作成します。どちらもTestExecutionListenersにて作成したリスナを読み込ませてください。また、SpringBootTestpropertiesでプロパティを設定します。

OracleをJDBCドライバに設定しているテストクラス

問題がなければテストが実行されます。

@TestExecutionListeners(OracleOnlyTestExecutionListener.class)
@SpringBootTest(properties = {
    "spring.datasource.driver-class-name:oracle.jdbc.OracleDriver",})
public class OracleOnlyTestExecutionListenerOracleTests {

  @OracleOnly
  @Test
  void test_02() {
    assertTrue(true, "実行すること");
  }
}

PostgreSQLをJDBCドライバに設定しているテストクラス

JDBCドライバがPostgreSQLのため、実行しません。実行した場合、アノテーションの期待していない挙動とみなすためにAssert.fail()を実行しています。

@TestExecutionListeners(OracleOnlyTestExecutionListener.class)
@SpringBootTest(properties = {
    "spring.datasource.driver-class-name:org.postgresql.Driver",})
public class OracleOnlyTestExecutionListenerPostgresTests {

  @OracleOnly
  @Test
  void test_01() {
    fail();
  }
}

ソースコード

※実際にspring.datasource.driver-class-nameを設定するとJDBCドライバが必要となるため、サンプルコードではspring.dummy-datasource.driver-class-nameとしています。

終わりに

この記事は、概要がすべてです。他の内容は全部蛇足。でも、ユースケースを伝えたほうが伝わると思いますので、こちらの書き方をしています。


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

類似

f:id:nainaistar:20210811081531p:plain