Mutant provides a hooks system that allows to inject custom behavior at critical points in the mutation testing execution pipeline. This is useful for setting up worker-specific resources, instrumenting mutations, or customizing the testing environment.
Mutant provides 8 different hook types that fire at various stages of execution:
env_infection_pre- Runs before environment infection (loading requires/includes)- Payload:
env:(theMutant::Envobject)
- Payload:
env_infection_post- Runs after environment infection- Payload:
env:(theMutant::Envobject)
- Payload:
setup_integration_pre- Runs before test integration setup- Payload: None
setup_integration_post- Runs after test integration setup- Payload: None
mutation_insert_pre- Runs before a mutation is inserted into the code- Payload:
mutation:(theMutant::Mutationobject)
- Payload:
mutation_insert_post- Runs after a mutation is inserted- Payload:
mutation:(theMutant::Mutationobject)
- Payload:
mutation_worker_process_start- Runs when a mutation worker process starts- Payload:
index:(the worker process index number)
- Payload:
test_worker_process_start- Runs when a test worker process starts- Payload:
index:(the worker process index number)
- Payload:
Hooks are configured in your mutant configuration file (.mutant.yml, config/mutant.yml, or mutant.yml) by specifying paths to hook files:
---
hooks:
- path/to/hooks_file_1.rb
- path/to/hooks_file_2.rbHook files are Ruby files that register hooks using the hooks.register method. Each hook receives a block that will be executed when the hook fires:
# Example: Log when mutations are inserted
hooks.register(:mutation_insert_pre) do |mutation:|
puts "About to insert mutation: #{mutation.identification}"
end
hooks.register(:mutation_insert_post) do |mutation:|
puts "Inserted mutation: #{mutation.identification}"
endOne of the most common use cases is setting up separate database resources for each worker process to avoid conflicts during parallel execution. Here's a complete example for Rails applications using PostgreSQL:
# rails_hooks.rb
hooks.register(:env_infection_post) do
Rails.application.eager_load!
end
hooks.register(:setup_integration_post) do
base_records.each do |base|
disconnect_pool(base:)
end
end
hooks.register(:test_worker_process_start) { |index:| isolate_index(index:) }
hooks.register(:mutation_worker_process_start) { |index:| isolate_index(index:) }
def self.base_records
[
ActiveRecord::Base,
]
end
def self.isolate_index(index:)
base_records.each do |base|
disconnect_pool(base:)
isolate_database(base:, index:)
end
end
def self.isolate_database(base:, index:)
db_config = base
.connection_handler
.retrieve_connection_pool(base.connection_specification_name)
.db_config
raw_template_database = db_config.database
raw_isolated_database = "#{raw_template_database}_mutant_worker_#{index}"
with_root_connection do |connection|
template_database = PG::Connection.quote_ident(raw_template_database)
isolated_database = PG::Connection.quote_ident(raw_isolated_database)
connection.exec_query("DROP DATABASE IF EXISTS #{isolated_database}")
connection.exec_query("CREATE DATABASE #{isolated_database} TEMPLATE #{template_database}")
end
db_config._database = raw_isolated_database
end
def self.disconnect_pool(base:)
base
.connection_handler
.retrieve_connection_pool(base.connection_specification_name)
.disconnect
end
def self.with_root_connection
base = ActiveRecord::Base
pool = base
.connection_handler
.retrieve_connection_pool(base.connection_specification_name)
connection = base
.postgresql_connection(**pool.db_config.configuration_hash, database: 'postgres')
yield connection
connection.disconnect!
endThis example:
- Eager loads the Rails application after environment infection
- Disconnects database pools after integration setup
- Creates isolated PostgreSQL databases for each worker using the test database as a template
- Ensures parallel workers don't conflict with each other's database operations
- Note: This database isolation pattern also enables parallel test runs without mutation testing
You can instrument mutations for logging, tracing, or debugging:
hooks.register(:mutation_insert_pre) do |mutation:|
# Log mutation details to a file
File.open('mutation_log.txt', 'a') do |f|
f.puts "#{Time.now}: Testing #{mutation.identification}"
end
endUse integration hooks to configure your test environment:
hooks.register(:setup_integration_pre) do
# Perform custom setup before test framework is initialized
load_custom_helpers
configure_test_environment
end
hooks.register(:setup_integration_post) do
# Verify test framework is properly configured
validate_test_configuration
endCustomize how your application loads:
hooks.register(:env_infection_pre) do |env:|
# Set up special loading requirements
require 'custom_loader'
end
hooks.register(:env_infection_post) do |env:|
# Verify environment is properly loaded
validate_application_state
endWhen multiple hooks are registered for the same event:
- Hooks from files are loaded in the order specified in the configuration
- Within each file, hooks are registered in the order they appear
- All hooks for an event execute sequentially in registration order
Example:
# In first_hooks.rb
hooks.register(:mutation_insert_pre) do |mutation:|
puts "First hook"
end
# In second_hooks.rb
hooks.register(:mutation_insert_pre) do |mutation:|
puts "Second hook"
endWith configuration:
hooks:
- first_hooks.rb
- second_hooks.rbOutput when mutation is inserted:
First hook
Second hook
- Hooks are implemented in
lib/mutant/hooks.rb - The
Mutant::Hooks::Builderclass is used during hook file evaluation - All hook data structures are immutable (frozen) after creation
- Hook files are evaluated using
binding.eval()with the builder as context - Unknown hook names raise
Mutant::Hooks::UnknownHookerror
Specifying an invalid hook name will raise an error:
hooks.register(:invalid_hook_name) do
# This will raise: Mutant::Hooks::UnknownHook: Unknown hook :invalid_hook_name
endValid hook names are limited to the 8 hooks listed at the top of this document.
- Hook blocks should be idempotent when possible
- Avoid long-running operations in hooks as they will slow down mutation testing
- Worker process hooks run in isolated child processes
- Environment and integration hooks run in the main process
- Mutation hooks run for each mutation being tested