2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1年の振り返りAdvent Calendar 2024

Day 5

RSpecでDDLを伴うテストをしたらクリーンアップ処理がうまくいかなくなった

Last updated at Posted at 2024-12-10

はじめに

Rails + MySQLの構成で、RSpecを用いて動的にCREATE TABLEを行うような処理に対してテストを書きたかったのですが、MySQLのDDLの性質とRSpecのデータベースのクリーンアップ処理について正しく理解していないがためにハマったので、それについて書きます。

概要

以下のように、Rails Application越しに動的にテーブルデータを作成するような処理が必要でした。

class Hoge < ApplicationRecord
  def self.create_temp_table_data
    connection.drop_table(:temp) if connection.table_exists?(:temp)

    connection.create_table :temp do |t|
      t.string :temp_column
    end

    connection.execute("INSERT INTO `temp` (`temp_column`) VALUES ('Hello World')")
  end
end
RSpec.describe Hoge, type: :model do
  describe '.create_temp_table_data' do
    subject { described_class.create_temp_table_data }

    it 'create temporary table' do
      subject
      expect(described_class.connection.table_exists?(:temp)).to eq(true)
      expect(
        described_class.connection.execute("SELECT COUNT(*) FROM `temp`").size
      ).to eq(1)
    end
  end
end

上記のようにテストを書いて問題なく通ることを確認できましたが、クリーンアップ処理が正しく行われずにデータが残ってしまっていたために、他のテストで落ちてしまうケースが出てきました。

rspec-railsにおけるデータベースのクリーンアップ処理

rspec-railsを用いてテストを実行する場合、デフォルトではtransactionを用いてクリーンアップ処理が行われます。
exampleが終了したタイミングでrollbackすることによって、作成・更新・削除したデータベースの変更が永続化されることなく元に戻ります。

What it really means in Rails is "run every test method within a transaction." In the context of rspec-rails, it means "run every example within a transaction."

The idea is to start each example with a clean database, create whatever data is necessary for that example, and then remove that data by simply rolling back the transaction at the end of the example.

なんのテストにもなっていないですが一連の処理を実際に行い、ログを確認するとtransactionを用いてクリーンアップ処理が行われていることが確認できます。

RSpec.describe Hoge, type: :model do
  describe 'example' do
    it 'create a record' do
      Hoge.create(fuga: "fuga")
      expect(Hoge.count).to eq(1)
    end
  end
end
TRANSACTION (0.1ms)  BEGIN
TRANSACTION (0.1ms)  SAVEPOINT active_record_1
Hoge Create (1.1ms)  INSERT INTO `hoges` (`fuga`, `created_at`, `updated_at`) VALUES ('fuga', '2024-12-10 14:41:38.931764', '2024-12-10 14:41:38.931764')
TRANSACTION (2.3ms)  RELEASE SAVEPOINT active_record_1
Hoge Count (0.8ms)  SELECT COUNT(*) FROM `hoges`
TRANSACTION (1.5ms)  ROLLBACK

MySQLのDDLにおける暗黙のcommit

MySQLではCREATE TABLEDROP TABLEなどのDDLを実行する際に、暗黙的なcommitが実行されます。なのでDDLに対するtransactionは効かないことになります。

  • DML
mysql> SELECT * FROM `hoges`;
Empty set (0.00 sec)

mysql> BEGIN;
mysql> INSERT INTO `hoges` (`fuga`, `created_at`, `updated_at`) VALUES ('hello world', '2020-01-01 10:10:10', '2020-01-01 10:10:10');
mysql> ROLLBACK;

-- Rollbackされたためテーブルにデータが存在しない
mysql> SELECT * FROM `hoges`;
Empty set (0.00 sec)
  • DDL
mysql> SHOW TABLES LIKE 'temp';
Empty set (0.00 sec)

mysql> CREATE TABLE `temp` (
    -> `id` bigint AUTO_INCREMENT,
    -> `temp_column` VARCHAR(255),
    -> PRIMARY KEY(`id`)
    -> );
mysql> ROLLBACK;

-- Robllbackされないため、テーブルが作成されている
mysql> show tables like 'temp';
+----------------------------------+
| Tables_in_app_development (temp) |
+----------------------------------+
| temp                             |
+----------------------------------+
1 row in set (0.01 sec)

クリーンアップ処理がうまくいかなかった理由

クリーンアップ処理の戦略としてtransactionを用いている中で、transactionが効かないDDLを実行していたからでした。
はじめに掲載したテストのログを確認すると以下のようになります。

TRANSACTION (0.1ms)  BEGIN
(3.0ms)  DROP TABLE `temp`
(4.8ms)  CREATE TABLE `temp` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `temp_column` varchar(255))
(1.0ms)  INSERT INTO `temp` (`temp_column`) VALUES ('Hello World')
(0.9ms)  SELECT COUNT(*) FROM `temp`
TRANSACTION (0.2ms)  ROLLBACK

transaction, truncation, deletion

rspec-railsのデフォルトではtransactionでクリーンアップ処理が行われますが、外部ツールを使うことによってtransactionではない方法でクリーンアップ処理を行うことができます。

その際には設定ファイルを以下のように変更する必要があります。
https://github.com/rspec/rspec-rails/blob/main/features/Transactions.md#disabling-transactions

spec/rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = false
end

外部ツールとしてよく使われるのがdatabase_cleanerです。transactionだけでなくtruncationとdeletionも使うことができます。

spec/rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = false
  
  config.before(:suite) do
    DatabaseCleaner.strategy = # (:transaction|:truncation|:deletion)
  end
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

transaction, trunaction, deletionの中で最も早いのはtransactionのようなので、基本的にはtransactionを使うのが良さそうです。

For the SQL libraries the fastest option will be to use :transaction as transactions are simply rolled back. If you can use this strategy you should.

今回のようなDDLを伴うようなテストがある場合は、テストにタグをつけるなどしてexampleごとにクリーンアップ戦略を使い分けるのが良さそうでした。

spec/rails_helper.rb

RSpec.configure do |config|
  config.use_transactional_fixtures = false
  config.around(:each) do |example|
    DatabaseCleaner.strategy = if example.metadata[:deletion]
      :deletion
    else
      :transaction
    end
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end
RSpec.describe Hoge, type: :model do
  describe '.create_temp_table_data' do
    subject { described_class.create_temp_table_data }

    it 'create temporary table', :deletion do
      subject
      expect(described_class.connection.table_exists?(:temp)).to eq(true)
      expect(
        described_class.connection.execute("SELECT COUNT(*) FROM `temp`").size
      ).to eq(1)
    end
  end
end

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?