--- url: /guide/recipes/active_record_shared_connection.md --- # Active Record Shared Connection > 💀 This functionality has been removed in v1.0. **NOTE:** a similar functionality has been added to Rails since version 5.1 (see [PR](https://github.com/rails/rails/pull/28083)). You shouldn't use `ActiveRecordSharedConnection` with modern Rails, it could lead to unexpected behaviour (e.g., mutexes deadlocks). Active Record creates a connection per thread by default. That doesn't allow us to use `transactional_tests` feature in system (with Capybara) tests (since Capybara runs a web server in a separate thread). A common approach is to use `database_cleaner` with a non-transactional strategy (`truncation` / `deletion`). But that *cleaning* phase may affect tests run time (and usually does). Sharing the connection between threads would allows us to use transactional tests as always. ## Instructions In your `spec_helper.rb` (or `rails_helper.rb` if any): ```ruby require "test_prof/recipes/active_record_shared_connection" ``` That automatically enables *shared connection* mode. You can enable/disable it manually: ```ruby TestProf::ActiveRecordSharedConnection.enable! TestProf::ActiveRecordSharedConnection.disable! ``` --- --- url: /ja/guide/recipes/active_record_shared_connection.md --- # Active Record Shared Connection > 💀 This functionality has been removed in v1.0. **NOTE:** a similar functionality has been added to Rails since version 5.1 (see [PR](https://github.com/rails/rails/pull/28083)). You shouldn't use `ActiveRecordSharedConnection` with modern Rails, it could lead to unexpected behaviour (e.g., mutexes deadlocks). Active Record creates a connection per thread by default. That doesn't allow us to use `transactional_tests` feature in system (with Capybara) tests (since Capybara runs a web server in a separate thread). A common approach is to use `database_cleaner` with a non-transactional strategy (`truncation` / `deletion`). But that *cleaning* phase may affect tests run time (and usually does). Sharing the connection between threads would allows us to use transactional tests as always. ## Instructions In your `spec_helper.rb` (or `rails_helper.rb` if any): ```ruby require "test_prof/recipes/active_record_shared_connection" ``` That automatically enables *shared connection* mode. You can enable/disable it manually: ```ruby TestProf::ActiveRecordSharedConnection.enable! TestProf::ActiveRecordSharedConnection.disable! ``` --- --- url: /ru/guide/recipes/active_record_shared_connection.md --- # Active Record Shared Connection > Данный функционал был удалён в версии 1.0. **Примечание:** аналогичный функционал включён в Rails с версии 5.1 (см. [PR](https://github.com/rails/rails/pull/28083)). Поэтому мы крайне не рекомендуем использование `ActiveRecordSharedConnection` в современных версиях фреймворка. Active Record создаёт отдельное подключение к базе данных для каждого треда. В некоторых случаях мы запускаем больше одного треда в тестах (например, при использовании браузерных тестов с Capybara). Это приводит к тому, что мы не можем использовать транзакционные тесты (`transactional_tests`) для поддержания БД в *чистом* состоянии. Долгое время популярным способом решения данной проблемы было использование гема `database_cleaner` в режиме `truncation` или `deletion` для удаления данных после каждого теста. Данный подход работает значительно медленнее, чем использование транзакций, и приводит к увеличению времени выполнения тестов. Мы предлагаем решить эту проблему по-другому — использовать единое соединение с БД во всех тредах, тем снова делая возможным использование транзакций. ## Инструкция В вашем `spec_helper.rb` (или `rails_helper.rb`) укажите: ```ruby require "test_prof/recipes/active_record_shared_connection" ``` Это автоматически активирует режим единого соединения. Вы можете отключить/включить его, используя следующие методы: ```ruby TestProf::ActiveRecordSharedConnection.enable! TestProf::ActiveRecordSharedConnection.disable! ``` --- --- url: /zh-cn/guide/recipes/active_record_shared_connection.md --- # Active Record 共享连接 \> 💀 该功能在 v1.0 中已被移除。 **注意:** 自 Rails 5.1 起 (查看 [PR](https://github.com/rails/rails/pull/28083))已经添加了一个类似的功能。你不应该在现代 Rails 上使用 `ActiveRecordSharedConnection` ,它可能导致意料之外的行为(比如,互斥锁的死锁)。 Active Record 默认是每个线程创建一个连接。 这就不允许我们在系统测试(用 Capybara)使用 `transactional_tests` 功能(因为 Capybara 是在一个单独线程内运行 Web 服务器)。 一个通用方案是使用 `database_cleaner` 并以不带事务的策略(`truncation` / `deletion`)。但这个 *cleaning* phase 可能会影响测试运行时间(通常如此)。 在线程之间共享连接将使我们能够像往常一样使用事务测试。 ## 教学 在 `spec_helper.rb` (或 `rails_helper.rb`,如果有的话)中: ```ruby require "test_prof/recipes/active_record_shared_connection" ``` 这就自动启用了 *共享连接* 模式。 你也可以手动启用/禁用它: ```ruby TestProf::ActiveRecordSharedConnection.enable! TestProf::ActiveRecordSharedConnection.disable! ``` --- --- url: /zh-cn/guide/recipes/any_fixture.md --- # Any Fixture Fixtures 是一种提高你测试性能的上佳方式,但对于大型项目,它们非常难于维护。 我们提出了一个更通用的方案来为你的测试套件 lazy 生成 *全局* 状态—— AnyFixture。 有了 AnyFixture,对于数据生成你可以使用任何代码块,并且它会在测试运行结束时对其进行清理。 考虑如下范例: ```ruby # The best way to use AnyFixture is through RSpec shared contexts RSpec.shared_context "account", account: true do # You should call AnyFixture outside of transaction to re-use the same # data between examples before(:all) do # The provided name ("account") should be unique. @account = TestProf::AnyFixture.register(:account) do # Do anything here, AnyFixture keeps track of affected DB tables # For example, you can use factories here FactoryGirl.create(:account) # or with Fabrication Fabricate(:account) # or with plain old AR Account.create!(name: "test") end end # Use .register here to track the usage stats (see below) let(:account) { TestProf::AnyFixture.register(:account) } # Or hard-reload object if there is chance of in-place modification let(:account) { Account.find(TestProf::AnyFixture.register(:account).id) } end # Then in your tests # Active this fixture using a tag describe UsersController, :account do # ... end # This test also uses the same account record, # no double-creation describe PostsController, :account do # ... end ``` 这儿有一个现实的 [例子](http://bit.ly/any-fixture)。 ## 教学 ### RSpec 在 `spec_helper.rb`(或 `rails_helper.rb`,如果有的话)中: ```ruby require "test_prof/recipes/rspec/any_fixture" ``` 现在你可以在测试中使用 `TestProf::AnyFixture` 了。 ### Minitest 当把 AnyFixture 与 Minitest 一起使用时,你要自己负责在每个测试运行之后清理数据库。例如: ```ruby # test_helper.rb require "test_prof/any_fixture" at_exit { TestProf::AnyFixture.clean } ``` ## DSL 我们提供了一个可选的 *句法糖* (通过 Refinement)使定义 fixtures 变得更容易: ```ruby require "test_prof/any_fixture/dsl" # Enable DSL using TestProf::AnyFixture::DSL # and then you can use `fixture` method (which is just an alias for `TestProf::AnyFixture.register`) before(:all) { fixture(:account) } # You can also use it to fetch the record (instead of storing it in instance variable) let(:account) { fixture(:account) } ``` ## `ActiveRecord#refind` TestProf 也提供了一个扩展来 *硬重载* ActiveRecord 对象: ```ruby # instead of let(:account) { Account.find(fixture(:account).id) } # load refinement require "test_prof/ext/active_record_refind" using TestProf::Ext::ActiveRecordRefind let(:account) { fixture(:account).refind } ``` ## 临时禁用 fixtures 你的有些测试可能依赖着 *清理数据库*,因此把它们跟依赖 AnyFixture 的测试一起运行就可能造成失败。 你可以在运行一个特定测试用例或用例组时,使用 `:with_clean_fixture` 的共享 context 以禁用(或删除)所有创建的 fixture: ```ruby context "global state", :with_clean_fixture do # or include explicitly # include_context "any_fixture:clean" specify "table is empty or smth like this" do # ... end end ``` 这是如何工作的?它把测试用例组包裹在一个事务内(使用 [`before_all`](./before_all.md))并在运行测试用例之前调用 `TestProf::AnyFixture.clean`。 因此,这个 context 有那么一点儿 *重*。尽量避免这些情况并编写独立于全局状态的 specs。 ## 使用报告 `AnyFixture` 在测试运行期间收集了使用信息,所以可以在结束时做出报告: ```sh [TEST PROF INFO] AnyFixture usage stats: key build time hit count saved time user 00:00.004 4 00:00.017 post 00:00.002 1 00:00.002 Total time spent: 00:00.006 Total time saved: 00:00.019 Total time wasted: 00:00.000 ``` 报告默认是关闭的,要启用可设置 `TestProf::AnyFixture.config.reporting_enabled = true` (或者可以通过`TestProf::AnyFixture.report_stats`手动执行它)。 你也可以通过 `ANYFIXTURE_REPORT=1` 环境变量来启用它。 ## 使用自动生成的 SQL 导出文件 > @since v1.0, 实验性的 AnyFixture 被设计为在每个测试套件运行时生成数据(并在结束时清理掉)。这依然有着时间的消耗(比如,对于系统或性能测试);因此,我们想要进一步优化。 我们提供了另一种对测试数据提速的方式,称作`#register_dump`。它的工作方式类似于针对初次运行的`#register`:接收一个代码块,并追踪其内部所生成的 SQL 查询。然后,它生成一份 SQL 导出文本,表示其调用期间创建或修改的数据,并使用这份导出对随后的测试恢复数据库状态。 来看一个范例: ```ruby RSpec.shared_context "account", account: true do # You should call AnyFixture outside of transaction to re-use the same # data between examples before(:all) do # The block is called once per test run (similary to #register) TestProf::AnyFixture.register_dump("account") do # Do anything here, AnyFixture keeps track of affected DB tables # For example, you can use factories here account = FactoryGirl.create(:account, name: "test") # or with Fabrication account = Fabricate(:account, name: "test") # or with plain old AR account = Account.create!(name: "test") # updates are also tracked account.update!(tag: "sql-dump") end end # Here, we MUST use a custom way to retrieve a record: since we restore the data # from a plain SQL dump, we have no knowledge of Ruby objects let(:account) { Account.find_by!(name: "test") } end ``` 而下面是运行测试时所发生的: ```bash # first run $ bundle exec rspec # AnyFixture.register_dump is called: # - is SQL dump present? No # - run block and write all modifying queries to a new SQL dump # AnyFixture.clean is called: # - clean all the affected tables # second run $ bundle exec rspec # AnyFixture.register_dump is called: # - is SQL dump present? Yes # - restore dump (do not run block) # AnyFixture.clean is called: # - clean all the affected tables ``` ### 所需环境 目前,仅支持 PostgreSQL 12+ 和 SQLite3。 ### 导出文件失效 所生成的导出文件会因多种原因而过期:数据库 schema 变更,fixture 代码块升级,等等。要处理这些无效情况,我们使用文件内容的 digests 作为缓存 keys(导出文件名为后缀)。 默认情况下,AnyFixture 会监控`db/schema.rb`,`db/structure.sql`和名为`\#register_dump`的文件。 默认监控文件列表可以通过修改`default_dump_watch_paths`配置参数来变更: ```ruby TestProf::AnyFixture.configure do |config| # you can use exact file paths or globs config.default_dump_watch_paths << Rails.root.join("spec/factories/**/*") end ``` 另外,你也可以通过`watch`选项把监控文件添加到一个特别的`#register_dump`调用上: ```ruby TestProf::AnyFixture.register_dump("account", watch: ["app/models/account.rb", "app/models/account/**/*,rb"]) do # ... end ``` **注意**:当你使用`watch`选项时,当前文件并未被添加到监控列表里。你要使用`__FILE__`来明确指定它。 最后,如果你想要强制重新生成导出文件,可以使用`ANYFIXTURE_FORCE_DUMP`环境变量: * `ANYFIXTURE_FORCE_DUMP=1`会强制全部导出文件都重新生成。 * `ANYFIXTURE_FORCE_DUMP=account`会强制重新生成仅匹配的导出文件(比如,匹配`/account/`的)。 #### 缓存 keys 可以提供自定义的缓存 keys,被用作 digest 的一部分: ```ruby # cache_key could be pretty much anything that responds to #to_s TestProf::AnyFixture.register_dump("account", cache_key: ["str", 1, {key: :val}]) do # ... end ``` ### Hooks #### `before` / `after` Before hooks 要么在调用一个 fixture 代码块 之前被调用,要么在恢复一个导出文件之前被调用。一种特别的使用场景是在多租户应用中重新创建一个租户: ```ruby TestProf::AnyFixture.register_dump( "account", before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end ) do # ... end ``` 类似地,after hooks 也是要么在调用一个 fixture 代码块 之前被调用,要么在恢复一个导出文件之前被调用。 你也能指定全局的 before 和 after hooks: ```ruby TestProf::AnyFixture.configure do |config| config.before_dump do |dump:, import:| # dump is an object containing information about the dump (e.g., dump.digest) # import is true if we're restoring a dump and false otherwise # do something end config.after_dump do |dump:, import:| # ... end end ``` **注意**:after 回调总是会执行,哪怕导出文件的创建失败了也会。你可以使用`dump.success?`方法来检测数据生成是否成功。 #### `skip_if` 该回调仅作为`#register_dump`的选项时可用,可被用来完全忽略 fixture。这在当你想要在测试运行之间保护数据库状态(比如,不清理数据库)的时候很有用。 下面是一个完整示例: ```ruby TestProf::AnyFixture.register_dump( "account", # do not track tables for AnyFixture.clean (though other fixtures could affect this) clean: false, skip_if: proc do |dump:| Apartment::Tenant.switch!("test") # if the current account has matching meta — the database is in actual state Account.find_by!(name: "test").meta["dump-version"] == dump.digest end, before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end, after: proc do |dump:, import:| # do not persist dump version if dump failed or we're restoring data next if import || !dump.success? Account.find_by!(name: "test").then do |account| account.meta["dump-version"] = dump.digest account.save! end end ) do # ... end ``` ### 配置 下面是一些可用的配置项: ```ruby TestProf::AnyFixture.configure do |config| # Where to store dumps (by default, TestProf.artifact_path + '/any_dumps') config.dumps_dir = "any_dumps" # Include mathing queries into a dump (in addition to INSERT/UPDATE/DELETE queries) config.dump_matching_queries = /^$/ # Whether to try using CLI tools such as psql or sqlite3 to restore dumps or not (and use ActiveRecord instead) config.import_dump_via_cli = false end ``` **注意**:当使用 CLI 工具来恢复导出文件时,无法追踪影响到的数据表,并因此无法通过`AnyFixture.clean`来清理它们。 --- --- url: /guide/recipes/any_fixture.md --- # AnyFixture Fixtures are a great way to increase your test suite performance, but for a large project, they are very hard to maintain. We propose a more general approach to lazy-generate the *global* state for your test suite – AnyFixture. With AnyFixture, you can use any block of code for data generation, and it will take care of cleaning it out at the end of the run. Consider an example: ```ruby # The best way to use AnyFixture is through RSpec shared contexts RSpec.shared_context "account", account: true do # You should call AnyFixture outside of transaction to re-use the same # data between examples before(:all) do # The provided name ("account") should be unique. @account = TestProf::AnyFixture.register(:account) do # Do anything here, AnyFixture keeps track of affected DB tables # For example, you can use factories here FactoryBot.create(:account) # or with Fabrication Fabricate(:account) # or with plain old AR Account.create!(name: "test") end end # Use .cached to retrieve the fixiture record let(:account) { TestProf::AnyFixture.cached(:account) } end # You can enhance the existing database cleaning. Posts will be deleted before fixtures reset TestProf::AnyFixture.before_fixtures_reset do Post.delete_all end # Or after reset TestProf::AnyFixture.after_fixtures_reset do Post.delete_all end # Then in your tests # Active this fixture using a tag describe UsersController, :account do # ... end # This test also uses the same account record, # no double-creation describe PostsController, :account do # ... end ``` See real life [example](http://bit.ly/any-fixture). ## Instructions ### RSpec In your `spec_helper.rb` (or `rails_helper.rb` if you have one): ```ruby require "test_prof/recipes/rspec/any_fixture" ``` Now you can use `TestProf::AnyFixture` in your tests. ### Minitest When using AnyFixture with Minitest, you should take care of cleaning the database after each test run by yourself. For example: ```ruby # test_helper.rb require "test_prof/any_fixture" at_exit { TestProf::AnyFixture.clean } ``` ## DSL We provide an optional *syntactic sugar* (through Refinement) to make it easier to define fixtures and use callbacks: ```ruby require "test_prof/any_fixture/dsl" # Enable DSL in RSpec RSpec.configure do |config| config.include TestProf::AnyFixture::DSL end # Minitest class MyBaseTestCase < Minitest::Test include TestProf::AnyFixture::DSL end # and then you can use `fixture` method (which is just an alias for `TestProf::AnyFixture.register`) before(:all) { fixture(:account) { create(:account) } } # You can also use it to fetch the record (instead of storing it in instance variable) let(:account) { fixture(:account) } # You can just use `before_fixtures_reset` or `after_fixtures_reset` callbacks before_fixtures_reset { Post.delete_all } after_fixtures_reset { Post.delete_all } ``` Note that the `#fixture` method also *refinds* Active Record objects on read, i.e., the following two expressions works similarly: ```ruby let(:account) { fixture(:account) } # similar to let(:account) { Account.find(TestProf::AnyFixture.cached(:account).id) } ``` ## Temporary disable fixtures Some of your tests might rely on *clean database*. Thus running them along with AnyFixture-dependent tests, could produce failures. You can disable (or delete) all created fixture while running a specified example or group using the `:with_clean_fixture` shared context: ```ruby context "global state", :with_clean_fixture do # or include explicitly # include_context "any_fixture:clean" specify "table is empty or smth like this" do # ... end end ``` How does it work? It wraps the example group into a transaction (using [`before_all`](./before_all.md)) and calls `TestProf::AnyFixture.clean` and `TestProf::AnyFixture.disable!` before running the examples and then call `TestProf::AnyFixture.enable!` at the context exit. The `disable!`/`enable!` method toggle the cache state. That makes it possible to re-use blocks passed during registration like there is no AnyFixture: ```ruby # Here we create a new account if AnyFixture is disabled let(:account) { fixture(:account) { create(:account) } } ``` Reseting fixtures (i.e., delete data from the affected tables) can be *heavy*. Try to avoid such situations and write specs independent of the global state. ## Usage report `AnyFixture` collects the usage information during the test run and could report it at the end: ```sh [TEST PROF INFO] AnyFixture usage stats: key build time hit count saved time user 00:00.004 4 00:00.017 post 00:00.002 1 00:00.002 Total time spent: 00:00.006 Total time saved: 00:00.019 Total time wasted: 00:00.000 ``` The reporting is off by default, to enable the reporting set `TestProf::AnyFixture.config.reporting_enabled = true` (or you can invoke it manually through `TestProf::AnyFixture.report_stats`). You can also enable reporting through the `ANYFIXTURE_REPORT=1` env variable. ## Using auto-generated SQL dumps > @since v1.0, experimental AnyFixture is designed to generate data once per a test suite run (and cleanup in the end). It still could be time-consuming (e.g., for system or performance tests); thus, we want to optimize further. We provide another way of speeding up test data called `#register_dump`. It works similarly to `#register` for the first run: it accepts a block of code and tracks SQL queries made within it. Then, it generates a plain SQL dump representing the data creating or modified during the call and uses this dump to restore the database state for the subsequent test runs. Let's consider an example: ```ruby RSpec.shared_context "account", account: true do # You should call AnyFixture outside of transaction to re-use the same # data between examples before(:all) do # The block is called once per test run (similary to #register) TestProf::AnyFixture.register_dump("account") do # Do anything here, AnyFixture keeps track of affected DB tables # For example, you can use factories here account = FactoryBot.create(:account, name: "test") # or with Fabrication account = Fabricate(:account, name: "test") # or with plain old AR account = Account.create!(name: "test") # updates are also tracked account.update!(tag: "sql-dump") end end # Here, we MUST use a custom way to retrieve a record: since we restore the data # from a plain SQL dump, we have no knowledge of Ruby objects let(:account) { Account.find_by!(name: "test") } end ``` And that's what happened when we run tests: ```sh # first run $ bundle exec rspec # AnyFixture.register_dump is called: # - is SQL dump present? No # - run block and write all modifying queries to a new SQL dump # AnyFixture.clean is called: # - clean all the affected tables # second run $ bundle exec rspec # AnyFixture.register_dump is called: # - is SQL dump present? Yes # - restore dump (do not run block) # AnyFixture.clean is called: # - clean all the affected tables ``` ### Requirements Currently, only PostgreSQL 12+ and SQLite3 are supported. ### Dump invalidation The generated dump could become out of date for many reasons: database schema changed, fixture block has been updated, etc. To deal with invalidation, we use file content digests as *cache keys* (dump file name suffixes). By default, AnyFixture *watches* `db/schema.rb`, `db/structure.sql` and the file that calls `#register_dump`. The list of default watch files could be updated by modifying the `default_dump_watch_paths` configuration parameter: ```ruby TestProf::AnyFixture.configure do |config| # you can use exact file paths or globs config.default_dump_watch_paths << Rails.root.join("spec/factories/**/*") end ``` Also, you add watch files to a specific `#register_dump` call via the `watch` option: ```ruby TestProf::AnyFixture.register_dump("account", watch: ["app/models/account.rb", "app/models/account/**/*,rb"]) do # ... end ``` **NOTE:** When you use the `watch` option, the current file is not added to the watch list. You should use `__FILE__` explicitly for that. Finally, if you want to forcefully re-generate a dump, you can use the `ANYFIXTURE_FORCE_DUMP` environment variable: * `ANYFIXTURE_FORCE_DUMP=1` will force all dumps regeneration. * `ANYFIXTURE_FORCE_DUMP=account` will force regeneration only of the matching dumps (i.e., matching `/account/`). #### Cache keys It's possible to provide custom cache keys to be used as a part of a digest: ```ruby # cache_key could be pretty much anything that responds to #to_s TestProf::AnyFixture.register_dump("account", cache_key: ["str", 1, {key: :val}]) do # ... end ``` ### Hooks #### `before` / `after` Before hooks are called either before calling a fixture block or before restoring a dump. One particular use case is to re-create a tenant in a multi-tenant app: ```ruby TestProf::AnyFixture.register_dump( "account", before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end ) do # ... end ``` Similarly, after hooks are called either after calling a fixture block or after restoring a dump. You can also specify global before and after hooks: ```ruby TestProf::AnyFixture.configure do |config| config.before_dump do |dump:, import:| # dump is an object containing information about the dump (e.g., dump.digest) # import is true if we're restoring a dump and false otherwise # do something end config.after_dump do |dump:, import:| # ... end end ``` **NOTE**: after callbacks are always executed, even if dump creation failed. You can use the `dump.success?` method to determine whether data generation succeeds or not. #### `skip_if` This callback is available only as of the `#register_dump` option and could be used to ignore the fixture completely. This is useful when you want to preserve the database state between test runs (i.e., do not clean the DB). Here is a complete example: ```ruby TestProf::AnyFixture.register_dump( "account", # do not track tables for AnyFixture.clean (though other fixtures could affect this) clean: false, skip_if: proc do |dump:| Apartment::Tenant.switch!("test") # if the current account has matching meta — the database is in actual state Account.find_by!(name: "test").meta["dump-version"] == dump.digest end, before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end, after: proc do |dump:, import:| # do not persist dump version if dump failed or we're restoring data next if import || !dump.success? Account.find_by!(name: "test").then do |account| account.meta["dump-version"] = dump.digest account.save! end end ) do # ... end ``` ### Configuration There a few more configuration options available: ```ruby TestProf::AnyFixture.configure do |config| # Where to store dumps (by default, TestProf.artifact_path + '/any_dumps') config.dumps_dir = "any_dumps" # Include mathing queries into a dump (in addition to INSERT/UPDATE/DELETE queries) config.dump_matching_queries = /^$/ # Whether to try using CLI tools such as psql or sqlite3 to restore dumps or not (and use ActiveRecord instead) config.import_dump_via_cli = false end ``` **NOTE:** When using CLI tools to restore dumps, it's not possible to track affected tables and thus clean them via `AnyFixture.clean`. --- --- url: /ja/guide/recipes/any_fixture.md --- # AnyFixture Fixtures are a great way to increase your test suite performance, but for a large project, they are very hard to maintain. We propose a more general approach to lazy-generate the *global* state for your test suite – AnyFixture. With AnyFixture, you can use any block of code for data generation, and it will take care of cleaning it out at the end of the run. Consider an example: ```ruby # The best way to use AnyFixture is through RSpec shared contexts RSpec.shared_context "account", account: true do # You should call AnyFixture outside of transaction to re-use the same # data between examples before(:all) do # The provided name ("account") should be unique. @account = TestProf::AnyFixture.register(:account) do # Do anything here, AnyFixture keeps track of affected DB tables # For example, you can use factories here FactoryBot.create(:account) # or with Fabrication Fabricate(:account) # or with plain old AR Account.create!(name: "test") end end # Use .register here to track the usage stats (see below) let(:account) { TestProf::AnyFixture.register(:account) } # Or hard-reload object if there is chance of in-place modification let(:account) { Account.find(TestProf::AnyFixture.register(:account).id) } end # You can enhance the existing database cleaning. Posts will be deleted before fixtures reset TestProf::AnyFixture.before_fixtures_reset do Post.delete_all end # Or after reset TestProf::AnyFixture.after_fixtures_reset do Post.delete_all end # Then in your tests # Active this fixture using a tag describe UsersController, :account do # ... end # This test also uses the same account record, # no double-creation describe PostsController, :account do # ... end ``` See real life [example](http://bit.ly/any-fixture). ## Instructions ### RSpec In your `spec_helper.rb` (or `rails_helper.rb` if you have one): ```ruby require "test_prof/recipes/rspec/any_fixture" ``` Now you can use `TestProf::AnyFixture` in your tests. ### Minitest When using AnyFixture with Minitest, you should take care of cleaning the database after each test run by yourself. For example: ```ruby # test_helper.rb require "test_prof/any_fixture" at_exit { TestProf::AnyFixture.clean } ``` ## DSL We provide an optional *syntactic sugar* (through Refinement) to make it easier to define fixtures and use callbacks: ```ruby require "test_prof/any_fixture/dsl" # Enable DSL using TestProf::AnyFixture::DSL # and then you can use `fixture` method (which is just an alias for `TestProf::AnyFixture.register`) before(:all) { fixture(:account) } # You can also use it to fetch the record (instead of storing it in instance variable) let(:account) { fixture(:account) } # You can just use `before_fixtures_reset` or `after_fixtures_reset` callbacks before_fixtures_reset { Post.delete_all } after_fixtures_reset { Post.delete_all } ``` ## `ActiveRecord#refind` TestProf also provides an extension to *hard-reload* ActiveRecord objects: ```ruby # instead of let(:account) { Account.find(fixture(:account).id) } # load refinement require "test_prof/ext/active_record_refind" using TestProf::Ext::ActiveRecordRefind let(:account) { fixture(:account).refind } ``` ## Temporary disable fixtures Some of your tests might rely on *clean database*. Thus running them along with AnyFixture-dependent tests, could produce failures. You can disable (or delete) all created fixture while running a specified example or group using the `:with_clean_fixture` shared context: ```ruby context "global state", :with_clean_fixture do # or include explicitly # include_context "any_fixture:clean" specify "table is empty or smth like this" do # ... end end ``` How does it work? It wraps the example group into a transaction (using [`before_all`](./before_all.md)) and calls `TestProf::AnyFixture.clean` before running the examples. Thus, this context is a little bit *heavy*. Try to avoid such situations and write specs independent of the global state. ## Usage report `AnyFixture` collects the usage information during the test run and could report it at the end: ```sh [TEST PROF INFO] AnyFixture usage stats: key build time hit count saved time user 00:00.004 4 00:00.017 post 00:00.002 1 00:00.002 Total time spent: 00:00.006 Total time saved: 00:00.019 Total time wasted: 00:00.000 ``` The reporting is off by default, to enable the reporting set `TestProf::AnyFixture.config.reporting_enabled = true` (or you can invoke it manually through `TestProf::AnyFixture.report_stats`). You can also enable reporting through the `ANYFIXTURE_REPORT=1` env variable. ## Using auto-generated SQL dumps > @since v1.0, experimental AnyFixture is designed to generate data once per a test suite run (and cleanup in the end). It still could be time-consuming (e.g., for system or performance tests); thus, we want to optimize further. We provide another way of speeding up test data called `#register_dump`. It works similarly to `#register` for the first run: it accepts a block of code and tracks SQL queries made within it. Then, it generates a plain SQL dump representing the data creating or modified during the call and uses this dump to restore the database state for the subsequent test runs. Let's consider an example: ```ruby RSpec.shared_context "account", account: true do # You should call AnyFixture outside of transaction to re-use the same # data between examples before(:all) do # The block is called once per test run (similary to #register) TestProf::AnyFixture.register_dump("account") do # Do anything here, AnyFixture keeps track of affected DB tables # For example, you can use factories here account = FactoryBot.create(:account, name: "test") # or with Fabrication account = Fabricate(:account, name: "test") # or with plain old AR account = Account.create!(name: "test") # updates are also tracked account.update!(tag: "sql-dump") end end # Here, we MUST use a custom way to retrieve a record: since we restore the data # from a plain SQL dump, we have no knowledge of Ruby objects let(:account) { Account.find_by!(name: "test") } end ``` And that's what happened when we run tests: ```sh # first run $ bundle exec rspec # AnyFixture.register_dump is called: # - is SQL dump present? No # - run block and write all modifying queries to a new SQL dump # AnyFixture.clean is called: # - clean all the affected tables # second run $ bundle exec rspec # AnyFixture.register_dump is called: # - is SQL dump present? Yes # - restore dump (do not run block) # AnyFixture.clean is called: # - clean all the affected tables ``` ### Requirements Currently, only PostgreSQL 12+ and SQLite3 are supported. ### Dump invalidation The generated dump could become out of date for many reasons: database schema changed, fixture block has been updated, etc. To deal with invalidation, we use file content digests as *cache keys* (dump file name suffixes). By default, AnyFixture *watches* `db/schema.rb`, `db/structure.sql` and the file that calls `#register_dump`. The list of default watch files could be updated by modifying the `default_dump_watch_paths` configuration parameter: ```ruby TestProf::AnyFixture.configure do |config| # you can use exact file paths or globs config.default_dump_watch_paths << Rails.root.join("spec/factories/**/*") end ``` Also, you add watch files to a specific `#register_dump` call via the `watch` option: ```ruby TestProf::AnyFixture.register_dump("account", watch: ["app/models/account.rb", "app/models/account/**/*,rb"]) do # ... end ``` **NOTE:** When you use the `watch` option, the current file is not added to the watch list. You should use `__FILE__` explicitly for that. Finally, if you want to forcefully re-generate a dump, you can use the `ANYFIXTURE_FORCE_DUMP` environment variable: * `ANYFIXTURE_FORCE_DUMP=1` will force all dumps regeneration. * `ANYFIXTURE_FORCE_DUMP=account` will force regeneration only of the matching dumps (i.e., matching `/account/`). #### Cache keys It's possible to provide custom cache keys to be used as a part of a digest: ```ruby # cache_key could be pretty much anything that responds to #to_s TestProf::AnyFixture.register_dump("account", cache_key: ["str", 1, {key: :val}]) do # ... end ``` ### Hooks #### `before` / `after` Before hooks are called either before calling a fixture block or before restoring a dump. One particular use case is to re-create a tenant in a multi-tenant app: ```ruby TestProf::AnyFixture.register_dump( "account", before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end ) do # ... end ``` Similarly, after hooks are called either after calling a fixture block or after restoring a dump. You can also specify global before and after hooks: ```ruby TestProf::AnyFixture.configure do |config| config.before_dump do |dump:, import:| # dump is an object containing information about the dump (e.g., dump.digest) # import is true if we're restoring a dump and false otherwise # do something end config.after_dump do |dump:, import:| # ... end end ``` **NOTE**: after callbacks are always executed, even if dump creation failed. You can use the `dump.success?` method to determine whether data generation succeeds or not. #### `skip_if` This callback is available only as of the `#register_dump` option and could be used to ignore the fixture completely. This is useful when you want to preserve the database state between test runs (i.e., do not clean the DB). Here is a complete example: ```ruby TestProf::AnyFixture.register_dump( "account", # do not track tables for AnyFixture.clean (though other fixtures could affect this) clean: false, skip_if: proc do |dump:| Apartment::Tenant.switch!("test") # if the current account has matching meta — the database is in actual state Account.find_by!(name: "test").meta["dump-version"] == dump.digest end, before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end, after: proc do |dump:, import:| # do not persist dump version if dump failed or we're restoring data next if import || !dump.success? Account.find_by!(name: "test").then do |account| account.meta["dump-version"] = dump.digest account.save! end end ) do # ... end ``` ### Configuration There a few more configuration options available: ```ruby TestProf::AnyFixture.configure do |config| # Where to store dumps (by default, TestProf.artifact_path + '/any_dumps') config.dumps_dir = "any_dumps" # Include mathing queries into a dump (in addition to INSERT/UPDATE/DELETE queries) config.dump_matching_queries = /^$/ # Whether to try using CLI tools such as psql or sqlite3 to restore dumps or not (and use ActiveRecord instead) config.import_dump_via_cli = false end ``` **NOTE:** When using CLI tools to restore dumps, it's not possible to track affected tables and thus clean them via `AnyFixture.clean`. --- --- url: /ru/guide/recipes/any_fixture.md --- # AnyFixture Фикстуры в Rails работает ни в пример быстрее фабрик, однако за скорость приходится платить меньшей гибкостью и временем на поддержку. Прежде всего это связано с тем, что фикстуры используют декларативный подход (YAML файлы с описанием данных). Мы предлагаем альтернативный подход к генерации глобального состояния для тестов – AnyFixture. AnyFixture позволяет использовать любой блок кода в качестве фикстуры. При этом не нужно думать о *чистоте* базы данных — AnyFixture позаботится о *зачистке* затронутых таблиц после выполнения всех тестов. Рассмотрим пример: ```ruby # Мы рекомендуем использовать контексты для подключения фикстур RSpec.shared_context "account" do # Внимание! Вызов AnyFixture должен происходит вне транзакций; # для этого мы используем before(:all) before(:all) do # Имя ("account") — это уникальный идентификатор фикстуры @account = TestProf::AnyFixture.register(:account) do # Внутри блока вы можете гененировать данные любым удобным вам способом. # Например, с помощью FactoryBot FactoryBot.create(:account) # или Fabrication Fabricate(:account) # или ActiveRecord Account.create!(name: "test") end end # Повторный вызов .register вернёт ранее созданный объект let(:account) { TestProf::AnyFixture.register(:account) } # Рекомендуется «пересоздавать» объект для избежания утечки состояния в тестах let(:account) { Account.find(TestProf::AnyFixture.register(:account).id) } end RSpec.configure do |config| config.include_context "account", account: true end # Активировать фикстуру можно, добави тег describe UsersController, :account do # ... end # Этот тест будет использовать тот же объект аккаунта describe PostsController, :account do # ... end ``` [Пример из боевого проекта](http://bit.ly/any-fixture). ## Инструкция ### RSpec В вашем `spec_helper.rb` (или `rails_helper.rb` при наличии) добавьте строчку: ```ruby require "test_prof/recipes/rspec/any_fixture" ``` Теперь модуль `TestProf::AnyFixture` доступен в ваших тестах. ### Minitest При использовании с Minitest вам необходимо вручную добавить код для зачистки базы после выполнения тестов. Например: ```ruby # test_helper.rb require "test_prof/any_fixture" at_exit { TestProf::AnyFixture.clean } ``` ## DSL AnyFixture предоставляет дополнительный DSL (*синтаксический сахар*) для более лаконичного определения фикстур: ```ruby require "test_prof/any_fixture/dsl" # Подключаем DSL с помощью refinements using TestProf::AnyFixture::DSL # Теперь вам доступен метод `fixture` (псевдоним для `TestProf::AnyFixture.register`) before(:all) { fixture(:account) { create(:account) } } # Он также может быть использован для доступа к уже созданному объекту let(:account) { fixture(:account) } ``` ## `ActiveRecord#refind` Для пересоздания объектов ActiveRecord (и избежания утечек состояния) вы можете использовать метод `#refind`, доступный через refinements: ```ruby # вместо let(:account) { Account.find(fixture(:account).id) } # загружаем refinement require "test_prof/ext/active_record_refind" using TestProf::Ext::ActiveRecordRefind let(:account) { fixture(:account).refind } ``` ## Временный откат фикстур Некоторые тесты могут требовать *чистой базы данных*, а значит их выполнение вместе с AnyFixture может приводить к ошибкам. Вы можете «отключить» фикстуры *локально* (т.е., очистить таблицы в БД), используя тег `:with_clean_fixture`: ```ruby context "global state", :with_clean_fixture do # либо подключив контекст явно # include_context "any_fixture:clean" specify "table is empty or smth like this" do # ... end end ``` Как это работает? Группа тестов оборачивается в транзакцию (с помощью [`before_all`](./before_all.md)), а затем вызывается `TestProf::AnyFixture.clean`. Зачистка — это довольно тяжёлая операция. Поэтому рекомендуется избегать подобных ситуаций, когда это возможно. ## Статистика использования AnyFixture позволяет выводит информацию о том, как часто фикстуры были использованы и сколько времени было сэкономлено: ```sh [TEST PROF INFO] AnyFixture usage stats: key build time hit count saved time user 00:00.004 4 00:00.017 post 00:00.002 1 00:00.002 Total time spent: 00:00.006 Total time saved: 00:00.019 Total time wasted: 00:00.000 ``` Вывод статистики выключен по умолчанию. Вы можете включить его в настройках (`TestProf::AnyFixture.config.reporting_enabled = true`) или указав переменную окружения `ANYFIXTURE_REPORT=1`. ## Использование автоматических SQL дампов > @since v1.0, experimental AnyFixture подразумевает генерацию данных для тестов единожды для каждого запуска. Однако даже это может занимать много времени (например, для системных тестов или тестов производительности, где, как правило, требуется много данных); значит, нам необходимо придумать, как ещё лучше оптимизировать популяцию тестовой базы данных! Представляем вашему вниманию ещё один метод AnyFixture—`#register_dump`. При первом запуске он работает аналогично `#register`: выполняет блок кода и отслеживает SQL запросы. Помимо этого AnyFixture также генерирует текстовый SQL дамп из этих запросов, и этот дамп будет использован для последующих запусков тестов — блок кода не будет выполнятся. Рассмотрим пример: ```ruby RSpec.shared_context "account", account: true do # Как и в случае .register, необходимо использовать .register_dump вне транзакций before(:all) do # Блок кода выполняется максимум один раз за весь запуск тестов. # Код не выполняется вообще, если уже существует соответствующий SQL дамп. TestProf::AnyFixture.register_dump("account") do account = FactoryGirl.create(:account, name: "test") account = Fabricate(:account, name: "test") account = Account.create!(name: "test") account.update!(tag: "sql-dump") end end # Так как данные могут быть загружены из SQL дампа, минуя Ruby код, # мы должны использовать специфичный код для доступа к объектам let(:account) { Account.find_by!(name: "test") } end ``` Когда мы запускаем тесты, происходит следующее: ```sh # первый запуск $ bundle exec rspec # Вызов AnyFixture.register_dump: # - Есть ли подходящий SQL дамп? Нет # - Выполняем блок кода, записываем все модифицирующие запросы в текстовый SQL дамп # Вызов AnyFixture.clean в конце запуска: # - удаляем данные из всех затронутых таблиц # повторной запуск $ bundle exec rspec # Вызов AnyFixture.register_dump: # - Есть ли подходящий SQL дамп? Да # - Восстанавливаем дамп # Вызов AnyFixture.clean в конце запуска: # - удаляем данные из всех затронутых таблиц ``` ### Требования В настоящий момент поддерживаются только PostgreSQL 12+ и SQLite3. ### Инвалидация дампов Сгенерированный дамп может стать неактуальным по множеству причин: схема БД изменилась, код фикстуры обновился и т.д.. Для отслеживания актуальности дампа AnyFixture использует хэш-суммы соответствующих файлов. По умолчанию это `db/schema.rb`, `db/structure.sql` и файл, в котором происходит вызов `#register_dump`. Вы можете изменить список файлов, за которыми нужно *следить* с помощью параметра `default_dump_watch_paths`: ```ruby TestProf::AnyFixture.configure do |config| # Можно использовать как точные пути, так и маски config.default_dump_watch_paths << Rails.root.join("spec/factories/**/*") end ``` Также вы можете указать дополнительные файлы при вызове `#register_dump`: ```ruby TestProf::AnyFixture.register_dump("account", watch: ["app/models/account.rb", "app/models/account/**/*,rb"]) do # ... end ``` **Примечание**: Когда вы используете опцию `watch`, текущий файл не добавляется автоматически в список отслеживаемых. Вы можете добавить его вручную, используя `__FILE__`. Наконец, если вы хотите умышленно пересоздать дамп, используйте переменную окружения `ANYFIXTURE_FORCE_DUMP`: * `ANYFIXTURE_FORCE_DUMP=1` пересоздаст все дампы. * `ANYFIXTURE_FORCE_DUMP=account` пересоздаст только дампы, принимаемые регулярным выражением `/account/`. #### Дополнительные ключи кэша Вы также можете указывать ключи для кэширования (в дополнение к хэш-суммам файлов): ```ruby # cache_key может быть чем угодно, отвечающим на #to_s TestProf::AnyFixture.register_dump("account", cache_key: ["str", 1, {key: :val}]) do # ... end ``` ### Хуки #### `before` / `after` Before хуки вызываются перед блоком кода (для первого запуска) или восстановлением дампа (для повторных запусков). Например, вы можете их использовать для пересоздания схемы в БД: ```ruby TestProf::AnyFixture.register_dump( "account", before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end ) do # ... end ``` Аналогичным образом, after хуки вызываются после выполнения блока кода или восстановления из дампа. Вы также можете указать глобальные хуки (т.е., вызываемые для всех фикстур): ```ruby TestProf::AnyFixture.configure do |config| config.before_dump do |dump:, import:| # dump — это объект, содержащий информацию о текущем дампе (например, dump.digest) # import — это флаг, который равен true тогда и только тогда, когда мы восстанавливаем данные из дампа end config.after_dump do |dump:, import:| # ... end end ``` **Примечание**: after выполняются всегда, даже если блок кода или восстановление дампа упали с ошибкой. Статус создания/восстановления дампа может быть получен с помощью метода `dump.success?`. #### `skip_if` Данный хук позволяет полностью пропустить инициализацию фикстуры (как из кода, так и из дампа). Это полезно, если вы хотите сохранить состояние базы данных между запусками тестов (т.е., не выполняете очистку). Пример: ```ruby TestProf::AnyFixture.register_dump( "account", # затронутые таблицы не будут добавлены в лист зачистки для AnyFixture.clean (однако другие фикстуры могут их добавить) clean: false, skip_if: proc do |dump:| Apartment::Tenant.switch!("test") # проверяем, актуальное ли состояние данных в базе Account.find_by!(name: "test").meta["dump-version"] == dump.digest end, before: proc do begin Apartment::Tenant.create("test") rescue nil end Apartment::Tenant.create("test") end, after: proc do |dump:, import:| next if import || !dump.success? Account.find_by!(name: "test").then do |account| account.meta["dump-version"] = dump.digest account.save! end end ) do # ... end ``` ### Настройка Доступны также следующие настройки: ```ruby TestProf::AnyFixture.configure do |config| # Куда сохранять дампы (относительно TestProf.artifact_path) config.dumps_dir = "any_dumps" # Включать в дамп подходящие под регулярное выражение запросы (в дополнение к INSERT/UPDATE/DELETE) config.dump_matching_queries = /^$/ # Использовать или нет консольные утилиты для восстановления дампов (psql или sqlite3) config.import_dump_via_cli = false end ``` **Примечание**: При использовании консольных утилит для восстановления дампов невозможно отследить выполняемые запросы и затронутые таблицы; следовательно, `AnyFixture.clean` не будет работать. --- --- url: /guide/recipes/before_all.md --- # Before All Rails has a great feature – `transactional_tests`, i.e. running each example within a transaction which is roll-backed in the end. Thus no example pollutes global database state. But what if have a lot of examples with a common setup? Of course, we can do something like this: ```ruby describe BeatleWeightedSearchQuery do before(:each) do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # and about 15 examples here end ``` Or you can try `before(:all)`: ```ruby describe BeatleWeightedSearchQuery do before(:all) do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` But then you have to deal with database cleaning, which can be either tricky or slow. There is a better option: we can wrap the whole example group into a transaction. And that's how `before_all` works: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` That's all! **NOTE**: requires RSpec >= 3.3.0. **NOTE**: Great superpower that `before_all` provides comes with a great responsibility. Make sure to check the [Caveats section](#caveats) of this document for details. ## Instructions ### Multiple database support The ActiveRecord BeforeAll adapter will only start a transaction using ActiveRecord::Base connection. If you want to ensure `before_all` can use multiple connections, you need to ensure the connection classes are loaded before using `before_all`. For example, imagine you have `ApplicationRecord` and a separate database for user accounts: ```ruby class Users < AccountsRecord # ... end class Articles < ApplicationRecord # ... end ``` Then those two Connection Classes do need to be loaded before the tests are run: ```ruby # Ensure connection classes are loaded ApplicationRecord AccountsRecord ``` This code can be added to `rails_helper.rb` or the rake tasks that runs minitests. ### RSpec In your `rails_helper.rb` (or `spec_helper.rb` after *ActiveRecord* has been loaded): ```ruby require "test_prof/recipes/rspec/before_all" ``` **NOTE**: `before_all` (and `let_it_be` that depends on it), does not wrap individual tests in a database transaction of its own. Use Rails' native `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1), RSpec Rails' `use_transactional_fixtures`, DatabaseCleaner, or custom code that begins a transaction before each test and rolls it back after. ### Minitest It is possible to use `before_all` with Minitest too: ```ruby require "test_prof/recipes/minitest/before_all" class MyBeatlesTest < Minitest::Test include TestProf::BeforeAll::Minitest before_all do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # define tests which could access the object defined within `before_all` end ``` In addition to `before_all`, TestProf also provides a `after_all` callback, which is called right before the transaction open by `before_all` is closed, i.e., after the last example from the test class completes. ## Database adapters You can use `before_all` not only with ActiveRecord (which is supported out-of-the-box) but with other database tools too. All you need is to build a custom adapter and configure `before_all` to use it: ```ruby class MyDBAdapter # before_all adapters must implement two methods: # - begin_transaction # - rollback_transaction def begin_transaction # ... end def rollback_transaction # ... end end # And then set adapter for `BeforeAll` module TestProf::BeforeAll.adapter = MyDBAdapter.new ``` ## Hooks You can register callbacks to run before/after `before_all` opens and rollbacks a transaction: ```ruby TestProf::BeforeAll.configure do |config| config.before(:begin) do # do something before transaction opens end # after(:begin) is also available config.after(:rollback) do # do something after transaction closes end # before(:rollback) is also available end ``` See the example in [Discourse](https://github.com/discourse/discourse/blob/4a1755b78092d198680c2fe8f402f236f476e132/spec/rails_helper.rb#L81-L141). ## Caveats ### Database is rolled back to a pristine state, but the objects are not If you modify objects generated within a `before_all` block in your examples, you maybe have to re-initiate them: ```ruby before_all do @user = create(:user) end let(:user) { @user } it "when user is admin" do # we modified our object in-place! user.update!(role: 1) expect(user).to be_admin end it "when user is regular" do # now @user's state depends on the order of specs! expect(user).not_to be_admin end ``` The easiest way to solve this is to reload record for every example (it's still much faster than creating a new one): ```ruby before_all do @user = create(:user) end # Note, that @user.reload may not be enough, # 'cause it doesn't reset associations let(:user) { User.find(@user.id) } # or with Minitest def setup @user = User.find(@user.id) end ``` ### Database is not rolled back between tests Database is not rolled back between RSpec examples, only between example groups. We don't want to reinvent the wheel and encourage you to use other tools that provide this out of the box. If you're using RSpec Rails, turn on `RSpec.configuration.use_transactional_fixtures` in your `spec/rails_helper.rb`: ```ruby RSpec.configure do |config| config.use_transactional_fixtures = true # RSpec takes care to use `use_transactional_tests` or `use_transactional_fixtures` depending on the Rails version used end ``` Make sure to set `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1) to `true` if you're using Minitest. If you're using DatabaseCleaner, make sure it rolls back the database between tests. ## Usage with Isolator [Isolator](https://github.com/palkan/isolator) is a runtime detector of potential atomicity breaches within DB transactions (e.g. making HTTP calls or enqueuing background jobs). TestProf recognizes Isolator out-of-the-box and make it ignore `before_all` transactions. You just need to make sure that you require `isolator` before loading `before_all` (or `let_it_be`). Alternatively, you can load the patch explicitly: ```ruby # after loading before_all or/and let_it_be require "test_prof/before_all/isolator" ``` ## Using Rails fixtures (*experimental*) If you want to use fixtures within a `before_all` hook, you must explicitly opt-in via `setup_fixture:` option: ```ruby before_all(setup_fixtures: true) do @user = users(:john) @post = create(:post, user: user) end ``` Works for both Minitest and RSpec. You can also enable fixtures globally (i.e., for all `before_all` hooks): ```ruby TestProf::BeforeAll.configure do |config| config.setup_fixtures = true end ``` ## Global Tags You can register callbacks for specific RSpec Example Groups using tags: ```ruby TestProf::BeforeAll.configure do |config| config.before(:begin, reset_sequences: true, foo: :bar) do warn <<~MESSAGE Do NOT create objects outside of transaction because all db sequences will be reset to 1 in every single example, so that IDs of new objects can get into conflict with the long-living ones. MESSAGE end end ``` --- --- url: /ja/guide/recipes/before_all.md --- # Before All Rails has a great feature – `transactional_tests`, i.e. running each example within a transaction which is roll-backed in the end. Thus no example pollutes global database state. But what if have a lot of examples with a common setup? Of course, we can do something like this: ```ruby describe BeatleWeightedSearchQuery do before(:each) do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # and about 15 examples here end ``` Or you can try `before(:all)`: ```ruby describe BeatleWeightedSearchQuery do before(:all) do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` But then you have to deal with database cleaning, which can be either tricky or slow. There is a better option: we can wrap the whole example group into a transaction. And that's how `before_all` works: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` That's all! **NOTE**: requires RSpec >= 3.3.0. **NOTE**: Great superpower that `before_all` provides comes with a great responsibility. Make sure to check the [Caveats section](#caveats) of this document for details. ## Instructions ### Multiple database support The ActiveRecord BeforeAll adapter will only start a transaction using ActiveRecord::Base connection. If you want to ensure `before_all` can use multiple connections, you need to ensure the connection classes are loaded before using `before_all`. For example, imagine you have `ApplicationRecord` and a separate database for user accounts: ```ruby class Users < AccountsRecord # ... end class Articles < ApplicationRecord # ... end ``` Then those two Connection Classes do need to be loaded before the tests are run: ```ruby # Ensure connection classes are loaded ApplicationRecord AccountsRecord ``` This code can be added to `rails_helper.rb` or the rake tasks that runs minitests. ### RSpec In your `rails_helper.rb` (or `spec_helper.rb` after *ActiveRecord* has been loaded): ```ruby require "test_prof/recipes/rspec/before_all" ``` **NOTE**: `before_all` (and `let_it_be` that depends on it), does not wrap individual tests in a database transaction of its own. Use Rails' native `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1), RSpec Rails' `use_transactional_fixtures`, DatabaseCleaner, or custom code that begins a transaction before each test and rolls it back after. ### Minitest It is possible to use `before_all` with Minitest too: ```ruby require "test_prof/recipes/minitest/before_all" class MyBeatlesTest < Minitest::Test include TestProf::BeforeAll::Minitest before_all do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # define tests which could access the object defined within `before_all` end ``` In addition to `before_all`, TestProf also provides a `after_all` callback, which is called right before the transaction open by `before_all` is closed, i.e., after the last example from the test class completes. ## Database adapters You can use `before_all` not only with ActiveRecord (which is supported out-of-the-box) but with other database tools too. All you need is to build a custom adapter and configure `before_all` to use it: ```ruby class MyDBAdapter # before_all adapters must implement two methods: # - begin_transaction # - rollback_transaction def begin_transaction # ... end def rollback_transaction # ... end end # And then set adapter for `BeforeAll` module TestProf::BeforeAll.adapter = MyDBAdapter.new ``` ## Hooks You can register callbacks to run before/after `before_all` opens and rollbacks a transaction: ```ruby TestProf::BeforeAll.configure do |config| config.before(:begin) do # do something before transaction opens end # after(:begin) is also available config.after(:rollback) do # do something after transaction closes end # before(:rollback) is also available end ``` See the example in [Discourse](https://github.com/discourse/discourse/blob/4a1755b78092d198680c2fe8f402f236f476e132/spec/rails_helper.rb#L81-L141). ## Caveats ### Database is rolled back to a pristine state, but the objects are not If you modify objects generated within a `before_all` block in your examples, you maybe have to re-initiate them: ```ruby before_all do @user = create(:user) end let(:user) { @user } it "when user is admin" do # we modified our object in-place! user.update!(role: 1) expect(user).to be_admin end it "when user is regular" do # now @user's state depends on the order of specs! expect(user).not_to be_admin end ``` The easiest way to solve this is to reload record for every example (it's still much faster than creating a new one): ```ruby before_all do @user = create(:user) end # Note, that @user.reload may not be enough, # 'cause it doesn't reset associations let(:user) { User.find(@user.id) } # or with Minitest def setup @user = User.find(@user.id) end ``` ### Database is not rolled back between tests Database is not rolled back between RSpec examples, only between example groups. We don't want to reinvent the wheel and encourage you to use other tools that provide this out of the box. If you're using RSpec Rails, turn on `RSpec.configuration.use_transactional_fixtures` in your `spec/rails_helper.rb`: ```ruby RSpec.configure do |config| config.use_transactional_fixtures = true # RSpec takes care to use `use_transactional_tests` or `use_transactional_fixtures` depending on the Rails version used end ``` Make sure to set `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1) to `true` if you're using Minitest. If you're using DatabaseCleaner, make sure it rolls back the database between tests. ## Usage with Isolator [Isolator](https://github.com/palkan/isolator) is a runtime detector of potential atomicity breaches within DB transactions (e.g. making HTTP calls or enqueuing background jobs). TestProf recognizes Isolator out-of-the-box and make it ignore `before_all` transactions. You just need to make sure that you require `isolator` before loading `before_all` (or `let_it_be`). Alternatively, you can load the patch explicitly: ```ruby # after loading before_all or/and let_it_be require "test_prof/before_all/isolator" ``` ## Using Rails fixtures (*experimental*) If you want to use fixtures within a `before_all` hook, you must explicitly opt-in via `setup_fixture:` option: ```ruby before_all(setup_fixtures: true) do @user = users(:john) @post = create(:post, user: user) end ``` Works for both Minitest and RSpec. You can also enable fixtures globally (i.e., for all `before_all` hooks): ```ruby TestProf::BeforeAll.configure do |config| config.setup_fixtures = true end ``` ## Global Tags You can register callbacks for specific RSpec Example Groups using tags: ```ruby TestProf::BeforeAll.configure do |config| config.before(:begin, reset_sequences: true, foo: :bar) do warn <<~MESSAGE Do NOT create objects outside of transaction because all db sequences will be reset to 1 in every single example, so that IDs of new objects can get into conflict with the long-living ones. MESSAGE end end ``` --- --- url: /ru/guide/recipes/before_all.md --- # Before All В Rails есть встроенная возможность запускать каждый тест внутри транзакции БД, которая автоматически откатывается после выполнения данного теста (`transactional_tests`). Таким образом, ни один тест не *загрязняет* базу данных, у нас не возникает *глобального состояния* (которое часто приводит к нестабильным тестам). Но что делать, если есть много тестов с общим контекстом? Конечно, мы можем сделать так: ```ruby describe BeatleWeightedSearchQuery do before(:each) do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # и около 15 тестов здесь end ``` Или попробовать `before(:all)`: ```ruby describe BeatleWeightedSearchQuery do before(:all) do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` Но тогда придется иметь дело с очисткой базы данных, т.к. `before(:all)` **вызывается вне транзакции**. *Подчищать* базу вручную, как правило, непросто или медленно (например, с помощью DatabaseCleaner). Есть вариант получше: мы можем **обернуть всю группу тестов в транзакцию**. Именно так и работает `before_all`: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` Вот и все! **Примечание**: требуется RSpec >= 3.3.0. **Примечание**: Как говорил дядя Питера Паркера: «Чем больше сила, тем больше ответственность». Обязательно проверьте [раздел «Предостережения»](#предостережения) для более подробной информации. ## Инструкция ### RSpec В вашем `rails_helper.rb` (или `spec_helper.rb`) **после загрузки ActiveRecord**: ```ruby require "test_prof/recipes/rspec/before_all" ``` **Примечание**: `before_all` (и [`let_it_be`](./let_it_be.md), который от него зависит), не оборачивает индивидуальные тесты в собственные транзакции базы данных. Используйте встроенный функционал Rails, `use_transactional_tests`,(`use_transactional_fixtures` в Rails < 5.1), либо `use_transactional_fixtures` из `rspec-rails`, либо DatabaseCleaner, либо свой код, который будет создавать транзакцию перед тестом и откатывать ее после. ### Minitest Можно также использовать `before_all` с Minitest: ```ruby require "test_prof/recipes/minitest/before_all" class MyBeatlesTest < Minitest::Test include TestProf::BeforeAll::Minitest before_all do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # можно определить тесты, которые используют объекты определённые в `before_all` end ``` В дополнение к `before_all` TestProf также предоставляет коллбек `after_all`, который вызывается непосредственное перед закрытиием транзакции, открытой в `before_all`, т.е., после выполнения последнего теста из класса (с учётом фильтров). ## Адаптеры баз данных Вы можете использовать `before_all` не только с ActiveRecord (который поддерживается из коробки), но и с другими инструментами для работы с базой данных. Все, что вам нужно, - это создать пользовательский адаптер и настроить `before_all` для его использования: ```ruby class MyDBAdapter # before_all адаптеры должны реализовывать два метода: # - begin_transaction # - rollback_transaction def begin_transaction # ... end def rollback_transaction # ... end end # А затем установите адаптер для `BeforeAll` модуля TestProf::BeforeAll.adapter = MyDBAdapter.new ``` ## Callback-функции Вы можете зарегистрировать callback-функции для событий открытия и отката транзакции \`before\_all: ```ruby TestProf::BeforeAll.configure do |config| config.before(:begin) do # что-то выполняется до открытия транзакции end # after(:begin) также доступен config.after(:rollback) do # что-то выполняется после закрытия транзакции end # before(:rollback) также доступен end ``` См. пример из [Discourse](https://github.com/discourse/discourse/blob/4a1755b78092d198680c2fe8f402f236f476e132/spec/rails_helper.rb#L81-L141). ## Предостережения ### База данных откатывается в первоначальное состояние, но объекты - нет Если внутри теста вы измените объекты, созданные в блоке `before_all`, то вам, возможно, придется повторно инициировать их: ```ruby before_all do @user = create(:user) end let(:user) { @user } it "when user is admin" do # мы изменили наш объект на месте! user.update!(role: 1) expect(user).to be_admin end it "when user is regular" do # теперь состояние @user зависит от порядка тестов! expect(user).not_to be_admin end ``` Самый простой способ решить эту проблему - перезагрузить запись для каждого теста (это все равно намного быстрее, чем создание новой): ```ruby before_all do @user = create(:user) end # Обратите внимание, что @user.reload может быть не достаточным, # потому что он не сбрасывает ассоциации let(:user) { User.find(@user.id) } # или в Minitest def setup @user = User.find(@user.id) end ``` ### База данных не откатывается между тестами База данных не откатывается между тестами, а только между группами тестов. Мы не хотим изобретать велосипед, а хотим поощрять использование других инструментов, которые предоставляют это из коробки. Если вы используете RSpec Rails, включите `RSpec.configuration.use_transactional_fixtures` в вашем `spec/rails_helper.rb`: ```ruby RSpec.configure do |config| # RSpec позаботится о том, чтобы использовать `use_transactional_tests` или # `use_transactional_fixtures` в зависимости от вашей версии Rails config.use_transactional_fixtures = true end ``` Убедитесь, что установлен `use_transactional_tests` (`use_transactional_fixtures` в Rails < 5.1) на `true`, если вы используете Minitest. Если вы используете DatabaseCleaner, убедитесь, что он откатывает базу данных между тестами. ## Использование с гемом Isolator [Isolator](https://github.com/palkan/isolator) — это инструмент для обнаружения потенциальных нарушений атомарности в транзакциях БД (например, выполнение HTTP-вызовов или постановка в очередь фоновых задач). TestProf распознает Isolator из коробки и делает так, чтобы он игнорировал `before_all` транзакции. Вам просто нужно убедиться, что вы подключили `isolator` перед загрузкой `before_all` или подключите следующий патч явно: ```ruby # После загрузки before_all и/или let_it_be require "test_prof/before_all/isolator" ``` ## Использование с Rails fixtures (*экспериментальная функция*) Если вы хотите использовать фикстуры в `before_all` коллбеках, вам необходимо явно их инициировать, используя опцию `setup_fixture:`: ```ruby before_all(setup_fixtures: true) do @user = users(:john) @post = create(:post, user: user) end ``` Данная опция поддерживается и в Minitest, и в RSpec. Вы также можете установить значение по умолчанию для `setup_fixtures`: ```ruby TestProf::BeforeAll.configure do |config| config.setup_fixtures = true end ``` --- --- url: /zh-cn/guide/recipes/before_all.md --- # Before All Rails 有一个很棒的特性——`transactional_tests`,比如,在一个事务之内运行每个测试用例,该用例会在结束时回滚。 这样就没有测试用例污染全局数据库状态了。 但如果在一个通用 setup 有许多用例的情况呢? 当然,我们可以象这样来做: ```ruby describe BeatleWeightedSearchQuery do before(:each) do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # and about 15 examples here end ``` 或者你也可以尝试 `before(:all)`: ```ruby describe BeatleWeightedSearchQuery do before(:all) do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` 但然后你就不得不处理数据库的清理工作,其要么乏味要么迟缓。 有一个更好的做法:我们可以把整个测试用例组包裹在一个事务内。 而这正是 `before_all` 工作的方式: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end # ... end ``` 就是如此! **注意**:需要 RSpec >= 3.3.0。 **注意**:`before_all` 所提供的强大能力要求你承担相应强大的责任。 确认查看了 [Caveats section](#caveats) 该文的具体细节。 ## 教学 ### RSpec 在你的 `rails_helper.rb`中(或 `spec_helper.rb` 中的 *ActiveRecord* 被加载之后): ```ruby require "test_prof/recipes/rspec/before_all" ``` **注意**:`before_all` (和依赖它的 `let_it_be`),未把单独的测试包裹在它自己的数据库事务中。请使用 Rails 原生的 `use_transactional_tests` (Rails < 5.1中是`use_transactional_fixtures` ),RSpec Rails 的`use_transactional_fixtures`, DatabaseCleaner,或者在每个测试用例前开始事务并在结束后回滚的自定义代码。 ### Minitest 可以与 Minitest 一起使用 `before_all` : ```ruby require "test_prof/recipes/minitest/before_all" class MyBeatlesTest < Minitest::Test include TestProf::BeforeAll::Minitest before_all do @paul = create(:beatle, name: "Paul") @ringo = create(:beatle, name: "Ringo") @george = create(:beatle, name: "George") @john = create(:beatle, name: "John") end # define tests which could access the object defined within `before_all` end ``` 除了`before_all`,TestProf 也提供了一个`after_all`回调,它在由`before_all`所打开的数据库事务关闭之前被调用,比如,在测试类的最后一个用例完成之后。 ## 数据库适配器 你可以不只跟 ActiveRecord(开箱即用)也可跟其他数据库工具一起使用 `before_all`。 你需要做的就是构建一个自定义适配器并配置 `before_all` 使用它: ```ruby class MyDBAdapter # before_all adapters must implement two methods: # - begin_transaction # - rollback_transaction def begin_transaction # ... end def rollback_transaction # ... end end # And then set adapter for `BeforeAll` module TestProf::BeforeAll.adapter = MyDBAdapter.new ``` ## Hooks > 自 v0.9.0 起 你可以在 `before_all` 打开和回滚一个事务之前/之后注册回调来运行它: ```ruby TestProf::BeforeAll.configure do |config| config.before(:begin) do # do something before transaction opens end # after(:begin) is also available config.after(:rollback) do # do something after transaction closes end # before(:rollback) is also available end ``` 请查看 [Discourse](https://github.com/discourse/discourse/blob/4a1755b78092d198680c2fe8f402f236f476e132/spec/rails_helper.rb#L81-L141) 中的范例。 ## 警告 ### 数据库是被回滚到全新的初始状态,但对象并非如此。 如果你在测试用例中更改了 `before_all` 代码块中所生成的对象,那么可能不得不重新初始化它们: ```ruby before_all do @user = create(:user) end let(:user) { @user } it "when user is admin" do # we modified our object in-place! user.update!(role: 1) expect(user).to be_admin end it "when user is regular" do # now @user's state depends on the order of specs! expect(user).not_to be_admin end ``` 解决这个问题最容易的方式是每个测试用例都重新加载记录(这\_仍然\_比创建一个新的要快得多): ```ruby before_all do @user = create(:user) end # Note, that @user.reload may not be enough, # 'cause it doesn't reset associations let(:user) { User.find(@user.id) } # or with Minitest def setup @user = User.find(@user.id) end ``` ### 数据库在测试之间没有回滚 数据库在 RSpec 测试用例之间不回滚,仅在测试用例组之间回滚。 我们不想重新发明轮子,所以鼓励你使用其他自带支持该功能的工具。 如果你使用 RSpec Rails,在你的 `spec/rails_helper.rb`中打开`RSpec.configuration.use_transactional_fixtures`: ```ruby RSpec.configure do |config| config.use_transactional_fixtures = true # RSpec takes care to use `use_transactional_tests` or `use_transactional_fixtures` depending on the Rails version used end ``` 如果你使用 Minitest,请确认设置了 `use_transactional_tests`(在 Rails < 5.1 中是`use_transactional_fixtures` )为 `true` 。 如果你在使用 DatabaseCleaner,请确认其是在测试之间进行回滚。 ## 与 Isolator 一起使用的方法 [Isolator](https://github.com/palkan/isolator) 是一个数据库事务内潜在违反原子性的运行时检测器(比如,进行 HTTP 调用或者后台队列作业)。 TestProf 自带对 Isolator 的识别并使其忽略 `before_all` 的事务。 你只需确保在加载 `before_all`(或 `let_it_be`)之前 require `isolator`。 要不然,你也可以明确地加载该补丁: ```ruby # after loading before_all or/and let_it_be require "test_prof/before_all/isolator" ``` ## 使用 Rails fixtures (*实验性的*) 如果你想要在`before_all` hook 内使用 fixture,必须通过`setup_fixture:`选项来明确加入: ```ruby before_all(setup_fixtures: true) do @user = users(:john) @post = create(:post, user: user) end ``` Minitest 和 RSpec 都可用。 你也可以全局启用 fixtures(比如,对所有`before_all` hooks): ```ruby TestProf::BeforeAll.configure do |config| config.setup_fixtures = true end ``` --- --- url: /guide/misc/rubocop.md --- # Custom RuboCop Cops TestProf comes with the [RuboCop](https://github.com/rubocop/rubocop) cops that help you write more performant tests. To enable them, add `test-prof` in your RuboCop configuration: ```yml # .rubocop.yml plugins: - 'test-prof' ``` Or you can just use it dynamically: ```sh bundle exec rubocop --plugin 'test-prof' --only RSpec/AggregateExamples ``` > \[!NOTE] > The plugin system is supported in RuboCop 1.72+. In earlier versions, use `require` instead of `plugins`. To configure cops to your needs: ```yml RSpec/AggregateExamples: AddAggregateFailuresMetadata: false ``` ## RSpec/AggregateExamples This cop encourages you to use one of the greatest features of the recent RSpec – aggregating failures within an example. Instead of writing one example per assertion, you can group *independent* assertions together, thus running all setup hooks only once. That can dramatically increase your performance (by reducing the total number of examples). Consider an example: ```ruby # bad it { is_expected.to be_success } it { is_expected.to have_header("X-TOTAL-PAGES", 10) } it { is_expected.to have_header("X-NEXT-PAGE", 2) } its(:status) { is_expected.to eq(200) } # good it "returns the second page", :aggregate_failures do is_expected.to be_success is_expected.to have_header("X-TOTAL-PAGES", 10) is_expected.to have_header("X-NEXT-PAGE", 2) expect(subject.status).to eq(200) end ``` Auto-correction will typically add `:aggregate_failures` to examples, but if your project enables it globally, or selectively by e.g. deriving metadata from file location, you may opt-out of adding it using `AddAggregateFailuresMetadata` config option. This cop supports auto-correct feature, so you can automatically refactor you legacy tests! **NOTE**: `its` examples shown here have been deprecated as of RSpec 3, but users of the [rspec-its gem](https://github.com/rspec/rspec-its) can leverage this cop to cut out that dependency. **NOTE**: auto-correction of examples using block matchers, such as `change` is deliberately not supported. --- --- url: /ja/guide/misc/rubocop.md --- # Custom RuboCop Cops TestProf comes with the [RuboCop](https://github.com/bbatsov/rubocop) cops that help you write more performant tests. To enable them, require `test_prof/rubocop` in your RuboCop configuration: ```yml # .rubocop.yml require: - 'test_prof/rubocop' ``` To configure cops to your needs: ```yml RSpec/AggregateExamples: AddAggregateFailuresMetadata: false ``` Or you can just require it dynamically: ```sh bundle exec rubocop -r 'test_prof/rubocop' --only RSpec/AggregateExamples ``` ## RSpec/AggregateExamples This cop encourages you to use one of the greatest features of the recent RSpec – aggregating failures within an example. Instead of writing one example per assertion, you can group *independent* assertions together, thus running all setup hooks only once. That can dramatically increase your performance (by reducing the total number of examples). Consider an example: ```ruby # bad it { is_expected.to be_success } it { is_expected.to have_header("X-TOTAL-PAGES", 10) } it { is_expected.to have_header("X-NEXT-PAGE", 2) } its(:status) { is_expected.to eq(200) } # good it "returns the second page", :aggregate_failures do is_expected.to be_success is_expected.to have_header("X-TOTAL-PAGES", 10) is_expected.to have_header("X-NEXT-PAGE", 2) expect(subject.status).to eq(200) end ``` Auto-correction will typically add `:aggregate_failures` to examples, but if your project enables it globally, or selectively by e.g. deriving metadata from file location, you may opt-out of adding it using `AddAggregateFailuresMetadata` config option. This cop supports auto-correct feature, so you can automatically refactor you legacy tests! **NOTE**: `its` examples shown here have been deprecated as of RSpec 3, but users of the [rspec-its gem](https://github.com/rspec/rspec-its) can leverage this cop to cut out that dependency. **NOTE**: auto-correction of examples using block matchers, such as `change` is deliberately not supported. --- --- url: /zh-cn/guide/profilers/event_prof.md --- # Event 分析器 EventProf 在你的测试套件运行期间收集各种测量指标(比如 ActiveSupport::Notifications)。 它的工作很类似于 `rspec --profile` 但可追踪任意事件。 输出范例: ```sh [TEST PROF INFO] EventProf results for sql.active_record Total time: 00:00.256 of 00:00.512 (50.00%) Total events: 1031 Top 5 slowest suites (by time): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – 00:00.119 (549 / 20) of 00:00.200 (59.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – 00:00.105 (360 / 18) of 00:00.125 (84.00%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – 00:00.032 (122 / 4) of 00:00.064 (50.00%) Top 5 slowest tests (by time): destroys question (./spec/controllers/questions_controller_spec.rb:38) – 00:00.022 (29) of 00:00.064 (34.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – 00:00.011 (34) of 00:00.022 (50.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (25) of 00:00.022 (36.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (32) of 00:00.035 (22.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – 00:00.007 (34) of 00:00.014 (50.00%) ``` ## 教学 目前,EventProf 仅支持 ActiveSupport::Notifications。 激活 EventProf: ### RSpec 使用 `EVENT_PROF` 环境变量设置为事件名称: ```sh # Collect SQL queries stats for every suite and example EVENT_PROF='sql.active_record' rspec ... ``` 你可以同时追踪多个事件: ```sh EVENT_PROF='sql.active_record,perform.active_job' rspec ... ``` ### Minitest 使用 `EVENT_PROF` 环境变量设置为事件名称: ```sh # Collect SQL queries stats for every suite and example EVENT_PROF='sql.active_record' rake test ``` 或者也可以使用 CLI 选项: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --event-prof=sql.active_record # Show the list of possible options: ruby test/my_super_test.rb --help ``` ### 与 Minitest::Reporters 一起使用 如果在你的项目中使用了 `Minitest::Reporters`,那么你必须明确在测试帮助方法文件中声明它: ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` #### 注意 当你以 gem 安装了 `minitest-reporters` 但并未在你的 `Gemfile` 里声明时, 确保总是在测试运行命令之前使用 `bundle exec` (但我们相信你总会这样做的)。 否则,你会得到由 Minitest plugin 系统造成的报错, 该系统对所有的 `minitest/*_plugin.rb` 扫描 `$LOAD_PATH` 内的入口, 因此该场景中可用的 `minitest-reporters` plugin 的初始化就不正确了。 如果你在用 Rails,参看 [Rails guides](http://guides.rubyonrails.org/active_support_instrumentation.html)来了解所有可用的事件。 如果你在用 [rom-rb](http://rom-rb.org),那么可能会对分析`'sql.rom'` 事件感兴趣。 ## 配置 默认情况下,EventProf 仅收集关于 top-level 组的信息(aka suites), 但你也可以分析单独的用例。只用设置好配置选项: ```ruby TestProf::EventProf.configure do |config| config.per_example = true end ``` 或者提供 `EVENT_PROF_EXAMPLES=1` 环境变量。 另一个有用但配置参数是——`rank_by`。它负责对统计数字排序——或者是事件耗费时间或者是出现次数: ```sh EVENT_PROF_RANK=count EVENT_PROF='instantiation.active_record' be rspec ``` 参看 [event\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/event_prof.rb) 了解所有可用配置选项及其用法。 ## 与 RSpecStamp 一起使用 EventProf 可跟 [RSpec Stamp](../recipes/rspec_stamp.md) 一起使用来以自定义 tag 自动标识\_慢\_测试用例,比如: ```sh EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ... ``` 运行上面命令之后,最慢的测试用例组(及测试用例,如果配置的话)就会用 `slow: :sql` 的 tag 进行标识。 ## 自定义测量器 要让 EventProf 和你的测量器引擎一起使用,只用完成下面两步: * 为你的测量器添加一个封装器: ```ruby # Wrapper over your instrumentation module MyEventsWrapper # Should contain the only one method def self.subscribe(event) raise ArgumentError, "Block is required!" unless block_given? ::MyEvents.subscribe(event) do |start, finish, *| yield (finish - start) end end end ``` * 在配置中设定测量器: ```ruby TestProf::EventProf.configure do |config| config.instrumenter = MyEventsWrapper end ``` ## 自定义事件 ### `"factory.create"` FactoryGirl 提供了两个它自己的测量器(`factory_girl.run_factory`),但有一个警告——每次一个 factory 被使用时就会触发一个事件,即使我们是为嵌套关联关系使用 factory。因此由于重复计算这就不可能计算 factories 所耗费的时间了。 EventProf 为 FactoryGirl 带来了一个小的补丁,其仅为 top-level 的 `FactoryGirl.create` 调用提供测量器。如果你使用 `"factory.create"` 事件,它会自动加载: ```sh EVENT_PROF=factory.create bundle exec rspec ``` > 自 v0.9.0 起 也支持 Fabrication(追踪隐式和显式的 `Fabricate.create` 调用)。 ### `"sidekiq.jobs"` 收集关于 inline 运行的 Sidekiq jobs 的统计数字: ```sh EVENT_PROF=sidekiq.jobs bundle exec rspec ``` **注意**: 会自动把 `rank_by` 设为 `count`(因为收集耗费时间的信息没有意义——参看下边)。 ### `"sidekiq.inline"` 收集关于 inline 运行的 Sidekiq jobs 的统计数字(除去嵌套 jobs): ```sh EVENT_PROF=sidekiq.inline bundle exec rspec ``` 使用该事件来分析正在运行的 Sidekiq jobs 的耗费时间。 ## 分析任意方法 你也可以添加自定义事件来分析特定的方法(例如,在用 [RubyProf](./ruby_prof.md) 或 [StackProf](./stack_prof.md)找出一些最热的调用后)。 比如,有个 class 做了些很繁重的工作: ```ruby class Work def do_smth(*args) # do something end end ``` 你可以通过添加一个 *monitor* 来分析: ```ruby # provide a class, event name and methods to monitor TestProf::EventProf.monitor(Work, "my.work", :do_smth) ``` 然后象平常一样运行 EventProf: ```sh EVENT_PROF=my.work bundle exec rake test ``` > 自 v0.9.0 起 你也可以提供额外的选项: * `top_level: true | false` (默认为`false`):定义是否只考虑 top-level 调用并忽略此事件的嵌套触发器(这正是 "factory.create" 如何[实现的](https://github.com/test-prof/test-prof/blob/master/lib/test_prof/event_prof/custom_events/factory_create.rb))。 * `guard: Proc` (默认为 `nil`): 提供一个 Proc,防止触发一个事件:仅当 `guard` 返回 `true`时方法才被测量; `guard` 使用 `instance_exec`执行,并将方法参数传递给它。 例如: ```ruby TestProf::EventProf.monitor( Sidekiq::Client, "sidekiq.inline", :raw_push, top_level: true, guard: ->(*) { Sidekiq::Testing.inline? } ) ``` 你可以根据\_需要\_添加 monitors(比如仅当你想要追踪特定事件时),通过把代码封装到 `TestProf::EventProf::CustomEvents.register`方法内: ```ruby TestProf::EventProf::CustomEvents.register("my.work") do TestProf::EventProf.monitor(Work, "my.work", :do_smth) end # Then call `activate_all` with the provided event TestProf::EventProf::CustomEvents.activate_all(TestProf::EventProf.config.event) ``` 这个 block 仅当特定事件在 EventProf 被启动时才执行。 --- --- url: /guide/profilers/event_prof.md --- # EventProf EventProf collects instrumentation (such as ActiveSupport::Notifications) metrics during your test suite run. It works very similar to `rspec --profile` but can track arbitrary events. Example output: ```sh [TEST PROF INFO] EventProf results for sql.active_record Total time: 00:00.256 of 00:00.512 (50.00%) Total events: 1031 Top 5 slowest suites (by time): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – 00:00.119 (549 / 20) of 00:00.200 (59.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – 00:00.105 (360 / 18) of 00:00.125 (84.00%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – 00:00.032 (122 / 4) of 00:00.064 (50.00%) Top 5 slowest tests (by time): destroys question (./spec/controllers/questions_controller_spec.rb:38) – 00:00.022 (29) of 00:00.064 (34.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – 00:00.011 (34) of 00:00.022 (50.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (25) of 00:00.022 (36.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (32) of 00:00.035 (22.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – 00:00.007 (34) of 00:00.014 (50.00%) ``` ## Instructions Currently, EventProf supports only ActiveSupport::Notifications To activate EventProf with: ### RSpec Use `EVENT_PROF` environment variable set to event name: ```sh # Collect SQL queries stats for every suite and example EVENT_PROF='sql.active_record' rspec ... ``` You can track multiple events simultaneously: ```sh EVENT_PROF='sql.active_record,perform.active_job' rspec ... ``` ### Minitest In Minitest 6+, you must first activate TestProf plugin by adding `Minitest.load :test_prof` in your test helper. Use `EVENT_PROF` environment variable set to event name: ```sh # Collect SQL queries stats for every suite and example EVENT_PROF='sql.active_record' rake test ``` or use CLI options as well: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --event-prof=sql.active_record # Show the list of possible options: ruby test/my_super_test.rb --help ``` ### Using with Minitest::Reporters If you're using `Minitest::Reporters` in your project you have to explicitly declare it in your test helper file: ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` #### NOTICE When you have `minitest-reporters` installed as a gem but not declared in your `Gemfile` make sure to always prepend your test run command with `bundle exec` (but we sure that you always do it). Otherwise, you'll get an error caused by Minitest plugin system, which scans all the entries in the `$LOAD_PATH` for any `minitest/*_plugin.rb`, thus initialization of `minitest-reporters` plugin which is available in that case doesn't happens correctly. See [Rails guides](http://guides.rubyonrails.org/active_support_instrumentation.html) for the list of available events if you're using Rails. If you're using [rom-rb](http://rom-rb.org) you might be interested in profiling `'sql.rom'` event. ## Configuration By default, EventProf collects information only about top-level groups (aka suites), but you can also profile individual examples. Just set the configuration option: ```ruby TestProf::EventProf.configure do |config| config.per_example = true end ``` Or provide the `EVENT_PROF_EXAMPLES=1` env variable. Another useful configuration parameter – `rank_by`. It's responsible for sorting stats – either by the time spent in the event or by the number of occurrences: ```sh EVENT_PROF_RANK=count EVENT_PROF='instantiation.active_record' be rspec ``` See [event\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/event_prof.rb) for all available configuration options and their usage. ## Using with RSpecStamp EventProf can be used with [RSpec Stamp](../recipes/rspec_stamp.md) to automatically mark *slow* examples with custom tags. For example: ```sh EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ... ``` After running the command above the slowest example groups (and examples if configured) would be marked with the `slow: :sql` tag. ## Custom Instrumentation To use EventProf with your instrumentation engine just complete the two following steps: * Add a wrapper for your instrumentation: ```ruby # Wrapper over your instrumentation module MyEventsWrapper # Should contain the only one method def self.subscribe(event) raise ArgumentError, "Block is required!" unless block_given? ::MyEvents.subscribe(event) do |start, finish, *| yield (finish - start) end end end ``` * Set instrumenter in the config: ```ruby TestProf::EventProf.configure do |config| config.instrumenter = MyEventsWrapper end ``` ## Custom Events ### `"factory.create"` FactoryBot provides its own instrumentation ('factory\_bot.run\_factory'); but there is a caveat – it fires an event every time a factory is used, even when we use factory for nested associations. Thus it's not possible to calculate the total time spent in factories due to the double calculation. EventProf comes with a little patch for FactoryBot which provides instrumentation only for top-level `FactoryBot.create` calls. It is loaded automatically if you use `"factory.create"` event: ```sh EVENT_PROF=factory.create bundle exec rspec ``` Also supports Fabrication (tracks implicit and explicit `Fabricate.create` calls). ### `"sidekiq.jobs"` Collects statistics about Sidekiq jobs that have been run inline: ```sh EVENT_PROF=sidekiq.jobs bundle exec rspec ``` **NOTE**: automatically sets `rank_by` to `count` ('cause it doesn't make sense to collect the information about time spent – see below). ### `"sidekiq.inline"` Collects statistics about Sidekiq jobs that have been run inline (excluding nested jobs): ```sh EVENT_PROF=sidekiq.inline bundle exec rspec ``` Use this event to profile the time spent running Sidekiq jobs. ## Profile arbitrary methods You can also add your custom events to profile specific methods (for example, after figuring out some hot calls with [RubyProf](./ruby_prof.md) or [StackProf](./stack_prof.md)). For example, having a class doing some heavy work: ```ruby class Work def do_smth(*args) # do something end end ``` You can profile it by adding a *monitor*: ```ruby # provide a class, event name and methods to monitor TestProf::EventProf.monitor(Work, "my.work", :do_smth) ``` And then run EventProf as usual: ```sh EVENT_PROF=my.work bundle exec rake test ``` You can also provide additional options: * `top_level: true | false` (defaults to `false`): defines whether you want to take into account only top-level invocations and ignore nested triggers of this event (that's how "factory.create" is [implemented](https://github.com/test-prof/test-prof/blob/master/lib/test_prof/event_prof/custom_events/factory_create.rb)) * `guard: Proc` (defaults to `nil`): provide a Proc which could prevent from triggering an event: the method is instrumented only if `guard` returns `true`; `guard` is executed using `instance_exec` and the method arguments are passed to it. For example: ```ruby TestProf::EventProf.monitor( Sidekiq::Client, "sidekiq.inline", :raw_push, top_level: true, guard: ->(*) { Sidekiq::Testing.inline? } ) ``` You can add monitors *on demand* (i.e. only when you want to track the specified event) by wrapping the code in `TestProf::EventProf::CustomEvents.register` method: ```ruby TestProf::EventProf::CustomEvents.register("my.work") do TestProf::EventProf.monitor(Work, "my.work", :do_smth) end # Then call `activate_all` with the provided event TestProf::EventProf::CustomEvents.activate_all(TestProf::EventProf.config.event) ``` The block is evaluated only if the specified event is enabled with EventProf. --- --- url: /ja/guide/profilers/event_prof.md --- # EventProf EventProf collects instrumentation (such as ActiveSupport::Notifications) metrics during your test suite run. It works very similar to `rspec --profile` but can track arbitrary events. Example output: ```sh [TEST PROF INFO] EventProf results for sql.active_record Total time: 00:00.256 of 00:00.512 (50.00%) Total events: 1031 Top 5 slowest suites (by time): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – 00:00.119 (549 / 20) of 00:00.200 (59.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – 00:00.105 (360 / 18) of 00:00.125 (84.00%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – 00:00.032 (122 / 4) of 00:00.064 (50.00%) Top 5 slowest tests (by time): destroys question (./spec/controllers/questions_controller_spec.rb:38) – 00:00.022 (29) of 00:00.064 (34.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – 00:00.011 (34) of 00:00.022 (50.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (25) of 00:00.022 (36.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (32) of 00:00.035 (22.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – 00:00.007 (34) of 00:00.014 (50.00%) ``` ## Instructions Currently, EventProf supports only ActiveSupport::Notifications To activate EventProf with: ### RSpec Use `EVENT_PROF` environment variable set to event name: ```sh # Collect SQL queries stats for every suite and example EVENT_PROF='sql.active_record' rspec ... ``` You can track multiple events simultaneously: ```sh EVENT_PROF='sql.active_record,perform.active_job' rspec ... ``` ### Minitest Use `EVENT_PROF` environment variable set to event name: ```sh # Collect SQL queries stats for every suite and example EVENT_PROF='sql.active_record' rake test ``` or use CLI options as well: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --event-prof=sql.active_record # Show the list of possible options: ruby test/my_super_test.rb --help ``` ### Using with Minitest::Reporters If you're using `Minitest::Reporters` in your project you have to explicitly declare it in your test helper file: ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` #### NOTICE When you have `minitest-reporters` installed as a gem but not declared in your `Gemfile` make sure to always prepend your test run command with `bundle exec` (but we sure that you always do it). Otherwise, you'll get an error caused by Minitest plugin system, which scans all the entries in the `$LOAD_PATH` for any `minitest/*_plugin.rb`, thus initialization of `minitest-reporters` plugin which is available in that case doesn't happens correctly. See [Rails guides](http://guides.rubyonrails.org/active_support_instrumentation.html) for the list of available events if you're using Rails. If you're using [rom-rb](http://rom-rb.org) you might be interested in profiling `'sql.rom'` event. ## Configuration By default, EventProf collects information only about top-level groups (aka suites), but you can also profile individual examples. Just set the configuration option: ```ruby TestProf::EventProf.configure do |config| config.per_example = true end ``` Or provide the `EVENT_PROF_EXAMPLES=1` env variable. Another useful configuration parameter – `rank_by`. It's responsible for sorting stats – either by the time spent in the event or by the number of occurrences: ```sh EVENT_PROF_RANK=count EVENT_PROF='instantiation.active_record' be rspec ``` See [event\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/event_prof.rb) for all available configuration options and their usage. ## Using with RSpecStamp EventProf can be used with [RSpec Stamp](../recipes/rspec_stamp.md) to automatically mark *slow* examples with custom tags. For example: ```sh EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ... ``` After running the command above the slowest example groups (and examples if configured) would be marked with the `slow: :sql` tag. ## Custom Instrumentation To use EventProf with your instrumentation engine just complete the two following steps: * Add a wrapper for your instrumentation: ```ruby # Wrapper over your instrumentation module MyEventsWrapper # Should contain the only one method def self.subscribe(event) raise ArgumentError, "Block is required!" unless block_given? ::MyEvents.subscribe(event) do |start, finish, *| yield (finish - start) end end end ``` * Set instrumenter in the config: ```ruby TestProf::EventProf.configure do |config| config.instrumenter = MyEventsWrapper end ``` ## Custom Events ### `"factory.create"` FactoryGirl provides its own instrumentation ('factory\_girl.run\_factory'); but there is a caveat – it fires an event every time a factory is used, even when we use factory for nested associations. Thus it's not possible to calculate the total time spent in factories due to the double calculation. EventProf comes with a little patch for FactoryGirl which provides instrumentation only for top-level `FactoryGirl.create` calls. It is loaded automatically if you use `"factory.create"` event: ```sh EVENT_PROF=factory.create bundle exec rspec ``` Also supports Fabrication (tracks implicit and explicit `Fabricate.create` calls). ### `"sidekiq.jobs"` Collects statistics about Sidekiq jobs that have been run inline: ```sh EVENT_PROF=sidekiq.jobs bundle exec rspec ``` **NOTE**: automatically sets `rank_by` to `count` ('cause it doesn't make sense to collect the information about time spent – see below). ### `"sidekiq.inline"` Collects statistics about Sidekiq jobs that have been run inline (excluding nested jobs): ```sh EVENT_PROF=sidekiq.inline bundle exec rspec ``` Use this event to profile the time spent running Sidekiq jobs. ## Profile arbitrary methods You can also add your custom events to profile specific methods (for example, after figuring out some hot calls with [RubyProf](./ruby_prof.md) or [StackProf](./stack_prof.md)). For example, having a class doing some heavy work: ```ruby class Work def do_smth(*args) # do something end end ``` You can profile it by adding a *monitor*: ```ruby # provide a class, event name and methods to monitor TestProf::EventProf.monitor(Work, "my.work", :do_smth) ``` And then run EventProf as usual: ```sh EVENT_PROF=my.work bundle exec rake test ``` You can also provide additional options: * `top_level: true | false` (defaults to `false`): defines whether you want to take into account only top-level invocations and ignore nested triggers of this event (that's how "factory.create" is [implemented](https://github.com/test-prof/test-prof/blob/master/lib/test_prof/event_prof/custom_events/factory_create.rb)) * `guard: Proc` (defaults to `nil`): provide a Proc which could prevent from triggering an event: the method is instrumented only if `guard` returns `true`; `guard` is executed using `instance_exec` and the method arguments are passed to it. For example: ```ruby TestProf::EventProf.monitor( Sidekiq::Client, "sidekiq.inline", :raw_push, top_level: true, guard: ->(*) { Sidekiq::Testing.inline? } ) ``` You can add monitors *on demand* (i.e. only when you want to track the specified event) by wrapping the code in `TestProf::EventProf::CustomEvents.register` method: ```ruby TestProf::EventProf::CustomEvents.register("my.work") do TestProf::EventProf.monitor(Work, "my.work", :do_smth) end # Then call `activate_all` with the provided event TestProf::EventProf::CustomEvents.activate_all(TestProf::EventProf.config.event) ``` The block is evaluated only if the specified event is enabled with EventProf. --- --- url: /ru/guide/profilers/event_prof.md --- # EventProf EventProf интегрируется с библиотеками для инструментации (такими как [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html)), позволяя собирать более детальную информацию о различных *событиях* в вашем коде (например, запросах к БД, создании фоновых задач, и т.д.). В результате вы получаете отчёт о том, в каких тестах и группах то или иное событие происходит чаще и занимает больше времени. Можно сказать, что это продвинутый `rspec --profile`, который умеет замерять не только общее время выполнения, но и время, потраченное на определённые действия. Наиболее полезен данный профилировщик для поиска тестов, в которых происходит чрезмерное взаимодействие с базой данных (событие `sql.active_record` в Rails) или, например, создание объектов с помощью фабрик в тестах (см. событие `factory.create` ниже). Пример отчёта: ```sh [TEST PROF INFO] EventProf results for sql.active_record Total time: 00:00.256 of 00:00.512 (50.00%) Total events: 1031 Top 5 slowest suites (by time): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – 00:00.119 (549 / 20) of 00:00.200 (59.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – 00:00.105 (360 / 18) of 00:00.125 (84.00%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – 00:00.032 (122 / 4) of 00:00.064 (50.00%) Top 5 slowest tests (by time): destroys question (./spec/controllers/questions_controller_spec.rb:38) – 00:00.022 (29) of 00:00.064 (34.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – 00:00.011 (34) of 00:00.022 (50.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (25) of 00:00.022 (36.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – 00:00.008 (32) of 00:00.035 (22.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – 00:00.007 (34) of 00:00.014 (50.00%) ``` Смотрите официальную [документацию Rails](http://guides.rubyonrails.org/active_support_instrumentation.html) для списка всех доступных событий. **Примечание**. Если вы используете [rom-rb](http://rom-rb.org), для профилирования времени, потраченного на запросы к БД, вам потребуется событие `'sql.rom'`. ## Инструкции EventProf поддерживает `ActiveSupport::Notifications` «из коробки». Используйте переменную окружения `EVENT_PROF` с указанием названия события для активации профилировщика: ```sh # Для RSpec EVENT_PROF='sql.active_record' rspec ... # Для Minitest аналогично EVENT_PROF='sql.active_record' rake test ``` Вы можете следить за несколькими событиями одновременно, указав их через запятую: ```sh EVENT_PROF='sql.active_record,perform.active_job' rspec ... ``` ### Minitest Если вы используете Minitest, вы также можете активировать EventProf, используя опцию CLI: ```sh ruby test/my_super_test.rb --event-prof=sql.active_record ``` ## Настройки По умолчанию EventProf агрегирует статистику по группам тестов (верхнеуровневые `describe` для RSpec, файлы для Minitest. Опционально вы можете включить профилирование отдельных тестов: ```ruby TestProf::EventProf.configure do |config| config.per_example = true end ``` Данную опцию можно также активировать с помощью переменной окружения `EVENT_PROF_EXAMPLES=1`. Другая полезная настройка — `rank_by`. Она отвечает за то, по какому показателю формировать *рейтинг*: затраченному времени (*time*) или количеству событий (*count*): ```sh EVENT_PROF_RANK=count EVENT_PROF='instantiation.active_record' be rspec ``` Все настройки доступны в [event\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/event_prof.rb). ## Интеграция с RSpecStamp EventProf интегрируется с [RSpec Stamp](../recipes/rspec_stamp.md) для автоматической пометки *медленных* тестов тегами. Например: ```sh EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ... ``` После запуска тестов с данными параметрами самые медленные группы тестов (или отдельные примеры, если выбрана данная опция) будут помечены тегом `slow: :sql`. ## Использование с другими библиотеками для инструментации Для того, чтобы использовать EventProf с отличными (от поддерживаемых) библиотеками для авторизации, необходимо выполнить следующие шаги: * Добавить класс-обёртку для целевой библиотеки, реализующей метод `#subscribe(event, &block)`, который вызывает переданный блок при возникновении события и передаёт потраченное время в качестве единственного аргумента: ```ruby module MyEventsWrapper def self.subscribe(event) raise ArgumentError, "Block is required!" unless block_given? ::MyEvents.subscribe(event) do |start, finish, *| yield (finish - start) end end end ``` * Указать данный класс в настройках: ```ruby TestProf::EventProf.configure do |config| config.instrumenter = MyEventsWrapper end ``` ## Дополнительные события EventProf также предоставляет собственную инструментацию (на базе ActiveSupport::Notifications) для некоторых событий, анализ которых особенно полезен при поиске проблемных мест в тестах. ### `"factory.create"` Несмотря на то, что гем FactoryBot предоставляет собственную инструментацию (событие `'factory_bot.run_factory'`), использовать её для целей учёта времени, потраченного на фабрики, просто так не получится – событие вызывается при каждом создании объекта с помощью фабрик, включая вложенные вызовы (например, когда у вас есть ассоциации). Это может привести к двойным вычислениям, а значит некорректным результатам. Для решения этой проблеме EventProf предоставляет собственное событие, которое вызывается только для *внешних* вызовов `FactoryBot#create`, — `'factory.create'`: ```sh EVENT_PROF=factory.create bundle exec rspec ``` Кроме того, данное событие универсально и работает также с библиотекой [Fabrication][]. ### `"sidekiq.jobs"` Данное событие возникает при выполнении задач Sidekiq **в режиме inline** (`Sidekiq::Testing.inline!`): ```sh EVENT_PROF=sidekiq.jobs bundle exec rspec ``` **Примечание**: для данного события автоматически включается сортировка по количеству событий (`rank_by=count`). ### `"sidekiq.inline"` Данное событие возникает при выполнении задач Sidekiq **в режиме inline** (`Sidekiq::Testing.inline!`). Данное событие отличается от `"sidekiq.jobs"` тем, что не вызывается только вложенных задач. Таким образом, предполагается использовать его для вычисления времени, потраченного *внутри* Sidekiq в тестах: ```sh EVENT_PROF=sidekiq.inline bundle exec rspec ``` ## Инструментация для произвольных методов Вы можете добавлять инструментацию произвольных методов любых классов в вашем приложении, т.е. замерять, сколько времени вы проводите *внутри* этих методов или как часто там оказываетесь в процессе выполнения тестов (например, после того, как вы обнаружили «тяжёлые» методы с помощью [RubyProf](./ruby_prof.md) или [StackProf](./stack_prof.md)). Допустим, у вас есть следующий класс: ```ruby class Work def do_smth(*args) # do something end end ``` Для того, чтобы добавить событие, возникающее при вызове метода `#do_smth` данного класса, вам необходимо добавить *монитор*: ```ruby # Первый аргумент — это класс, второй — название события, третий — имя метода TestProf::EventProf.monitor(Work, "my.work", :do_smth) ``` После этого вы можете использовать EventProf как обычно: ```sh EVENT_PROF=my.work bundle exec rake test ``` Доступны также следующие дополнительные опции при создании монитора: * `top_level: true | false` (по умолчанию `false`): определяет, учитывать только события верхнего уровня или включая вложенные (описанное выше событие `'factory.create'` как раз [использует эту опцию](https://github.com/test-prof/test-prof/blob/master/lib/test_prof/event_prof/custom_events/factory_create.rb)); * `guard: Proc` (по умолчанию `nil`): вы можете указать объект типа Proc/Lambda, который будет вызываться перед инициацией события; событие будет отправлено, только если блок вернёт `true`; `guard` выполняется в контексте вызова метода (через `instance_exec`) и принимает на вход аргументы, переданные в наблюдаемый метод. Рассмотрим пример: ```ruby TestProf::EventProf.monitor( Sidekiq::Client, "sidekiq.inline", :raw_push, top_level: true, guard: ->(*) { Sidekiq::Testing.inline? } ) ``` Чтобы добавлять патчи мониторов *по запросу* (т.е., только когда профилирование данного события включено), вы можете использовать специальный метод `TestProf::EventProf::CustomEvents.register`: ```ruby TestProf::EventProf::CustomEvents.register("my.work") do TestProf::EventProf.monitor(Work, "my.work", :do_smth) end ``` После регистрации всех событий необходимо переинициализировать EventProf, выполнив следующий код: ```ruby TestProf::EventProf::CustomEvents.activate_all(TestProf::EventProf.config.event) ``` [Fabrication]: https://www.fabricationgem.org --- --- url: /guide/profilers/factory_doctor.md --- # Factory Doctor One common bad pattern that slows our tests down is unnecessary database manipulation. Consider a *bad* example: ```ruby # with FactoryBot it "validates name presence" do user = create(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate(:user) user.name = "" expect(user).not_to be_valid end ``` Here we create a new user record, run all callbacks and validations and save it to the database. We don't need all these! Here is a *good* example: ```ruby # with FactoryBot it "validates name presence" do user = build_stubbed(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate.build(:user) user.name = "" expect(user).not_to be_valid end ``` Read more about [`build_stubbed`](https://thoughtbot.com/blog/use-factory-bots-build-stubbed-for-a-faster-test). FactoryDoctor is a tool that helps you identify such *bad* tests, i.e. tests that perform unnecessary database queries. Example output: ```sh [TEST PROF INFO] FactoryDoctor report Total (potentially) bad examples: 2 Total wasted time: 00:13.165 User (./spec/models/user_spec.rb:3) (3 records created, 00:00.628) validates name (./spec/user_spec.rb:8) – 1 record created, 00:00.114 validates email (./spec/user_spec.rb:8) – 2 records created, 00:00.514 ``` **NOTE**: have you noticed the "potentially" word? Unfortunately, FactoryDoctor is not a magician (it's still learning) and sometimes it produces false negatives and false positives too. Please, submit an [issue](https://github.com/test-prof/test-prof/issues) if you found a case which makes FactoryDoctor fail. You can also tell FactoryDoctor to ignore specific examples/groups. Just add the `:fd_ignore` tag to it: ```ruby # won't be reported as offense it "is ignored", :fd_ignore do user = create(:user) user.name = "" expect(user).not_to be_valid end ``` ## Instructions FactoryDoctor supports: * FactoryBot * Fabrication. ### RSpec To activate FactoryDoctor use `FDOC` environment variable: ```sh FDOC=1 rspec ... ``` ### Using with RSpecStamp FactoryDoctor can be used with [RSpec Stamp](../recipes/rspec_stamp.md) to automatically mark *bad* examples with custom tags. For example: ```sh FDOC=1 FDOC_STAMP="fdoc:consider" rspec ... ``` After running the command above all *potentially* bad examples would be marked with the `fdoc: :consider` tag. ### Minitest In Minitest 6+, you must first activate TestProf plugin by adding `Minitest.load :test_prof` in your test helper. To activate FactoryDoctor use `FDOC` environment variable: ```sh FDOC=1 ruby ... ``` or use CLI option as shown below: ```sh ruby ... --factory-doctor ``` The same option to force Factory Doctor to ignore specific examples is also available for Minitest. Just use `fd_ignore` inside your example: ```ruby # won't be reported as offense it "is ignored" do fd_ignore @user.name = "" refute @user.valid? end ``` ### Using with Minitest::Reporters If you're using `Minitest::Reporters` in your project you have to explicitly declare it in your test helper file: ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` **NOTE**: When you have `minitest-reporters` installed as a gem but not declared in your `Gemfile` make sure to always prepend your test run command with `bundle exec` (but we sure that you always do it). Otherwise, you'll get an error caused by Minitest plugin system, which scans all the entries in the `$LOAD_PATH` for any `minitest/*_plugin.rb`, thus initialization of `minitest-reporters` plugin which is available in that case doesn't happens correctly. ## Configuration The following configuration parameters are available (showing defaults): ```ruby TestProf::FactoryDoctor.configure do |config| # Which event to track within test example to consider them "DB-dirty" config.event = "sql.active_record" # Consider result "good" if the time in DB is less then the threshold config.threshold = 0.01 end ``` You can use the corresponding env variables as well: `FDOC_EVENT` and `FDOC_THRESHOLD`. --- --- url: /ja/guide/profilers/factory_doctor.md --- # Factory Doctor One common bad pattern that slows our tests down is unnecessary database manipulation. Consider a *bad* example: ```ruby # with FactoryBot/FactoryGirl it "validates name presence" do user = create(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate(:user) user.name = "" expect(user).not_to be_valid end ``` Here we create a new user record, run all callbacks and validations and save it to the database. We don't need all these! Here is a *good* example: ```ruby # with FactoryBot/FactoryGirl it "validates name presence" do user = build_stubbed(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate.build(:user) user.name = "" expect(user).not_to be_valid end ``` Read more about [`build_stubbed`](https://robots.thoughtbot.com/use-factory-girls-build-stubbed-for-a-faster-test). FactoryDoctor is a tool that helps you identify such *bad* tests, i.e. tests that perform unnecessary database queries. Example output: ```sh [TEST PROF INFO] FactoryDoctor report Total (potentially) bad examples: 2 Total wasted time: 00:13.165 User (./spec/models/user_spec.rb:3) (3 records created, 00:00.628) validates name (./spec/user_spec.rb:8) – 1 record created, 00:00.114 validates email (./spec/user_spec.rb:8) – 2 records created, 00:00.514 ``` **NOTE**: have you noticed the "potentially" word? Unfortunately, FactoryDoctor is not a magician (it's still learning) and sometimes it produces false negatives and false positives too. Please, submit an [issue](https://github.com/test-prof/test-prof/issues) if you found a case which makes FactoryDoctor fail. You can also tell FactoryDoctor to ignore specific examples/groups. Just add the `:fd_ignore` tag to it: ```ruby # won't be reported as offense it "is ignored", :fd_ignore do user = create(:user) user.name = "" expect(user).not_to be_valid end ``` ## Instructions FactoryDoctor supports: * FactoryGirl/FactoryBot * Fabrication. ### RSpec To activate FactoryDoctor use `FDOC` environment variable: ```sh FDOC=1 rspec ... ``` ### Using with RSpecStamp FactoryDoctor can be used with [RSpec Stamp](../recipes/rspec_stamp.md) to automatically mark *bad* examples with custom tags. For example: ```sh FDOC=1 FDOC_STAMP="fdoc:consider" rspec ... ``` After running the command above all *potentially* bad examples would be marked with the `fdoc: :consider` tag. ### Minitest To activate FactoryDoctor use `FDOC` environment variable: ```sh FDOC=1 ruby ... ``` or use CLI option as shown below: ```sh ruby ... --factory-doctor ``` The same option to force Factory Doctor to ignore specific examples is also available for Minitest. Just use `fd_ignore` inside your example: ```ruby # won't be reported as offense it "is ignored" do fd_ignore @user.name = "" refute @user.valid? end ``` ### Using with Minitest::Reporters If you're using `Minitest::Reporters` in your project you have to explicitly declare it in your test helper file: ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` **NOTE**: When you have `minitest-reporters` installed as a gem but not declared in your `Gemfile` make sure to always prepend your test run command with `bundle exec` (but we sure that you always do it). Otherwise, you'll get an error caused by Minitest plugin system, which scans all the entries in the `$LOAD_PATH` for any `minitest/*_plugin.rb`, thus initialization of `minitest-reporters` plugin which is available in that case doesn't happens correctly. ## Configuration The following configuration parameters are available (showing defaults): ```ruby TestProf::FactoryDoctor.configure do |config| # Which event to track within test example to consider them "DB-dirty" config.event = "sql.active_record" # Consider result "good" if the time in DB is less then the threshold config.threshold = 0.01 end ``` You can use the corresponding env variables as well: `FDOC_EVENT` and `FDOC_THRESHOLD`. --- --- url: /ru/guide/profilers/factory_doctor.md --- # Factory Doctor Одна из распространенных причин, замедляющих наши тесты, — это ненужные манипуляции с базой данных. Рассмотрим *плохой* пример: ```ruby # with FactoryBot/FactoryGirl it "validates name presence" do user = create(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate(:user) user.name = "" expect(user).not_to be_valid end ``` Здесь мы создаем новую запись, запуская callback-функции и валидации, и сохраняем её в базу данных. Нам всё это не нужно! Вот *хороший* пример: ```ruby # with FactoryBot/FactoryGirl it "validates name presence" do user = build_stubbed(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate.build(:user) user.name = "" expect(user).not_to be_valid end ``` Подробнее о [`build_stubbed`](https://robots.thoughtbot.com/use-factory-girls-build-stubbed-for-a-faster-test). FactoryDoctor помогает вам найти такие *плохие* тесты, т. е. тесты, которые выполняют ненужные запросы к базе данных. Пример вывода: ```sh [TEST PROF INFO] FactoryDoctor report Total (potentially) bad examples: 2 Total wasted time: 00:13.165 User (./spec/models/user_spec.rb:3) (3 records created, 00:00.628) validates name (./spec/user_spec.rb:8) – 1 record created, 00:00.114 validates email (./spec/user_spec.rb:8) – 2 records created, 00:00.514 ``` **Примечание**: вы обратили на слово «potentially» (потенциально)? К сожалению, FactoryDoctor не волшебник (он все еще учится), и иногда он выдает ложноотрицательные или ложноположительные результаты. Пожалуйста, создайте [issue](https://github.com/test-prof/test-prof/issues), если вы нашли пример, в котором FactoryDoctor ошибся. Вы можете исключить отдельные тесты из анализа, добавив тег `:fd_ignore`: ```ruby # данный тест не будет отмечен FactoryDoctor it "is ignored", :fd_ignore do user = create(:user) user.name = "" expect(user).not_to be_valid end ``` ## Инструкции FactoryDoctor поддерживает: * FactoryGirl/FactoryBot * Fabrication. ### RSpec Для активации FactoryDoctor используйте переменную окружения `FDOC`: ```sh FDOC=1 rspec ... ``` ### Использование с RSpecStamp FactoryDoctor может быть использован вместе с [RSpec Stamp](../recipes/rspec_stamp.md) для автоматического тегирования *плохих* тестов. Например: ```sh FDOC=1 FDOC_STAMP="fdoc:consider" rspec ... ``` После запуска команды выше все *потенциально* плохие тесты будут помечены тегом `fdoc: :consider`. ### Minitest Для активации FactoryDoctor используйте переменную окружения `FDOC`: ```sh FDOC=1 ruby ... ``` Или используйте опцию, как показано ниже: ```sh ruby ... --factory-doctor ``` Опция игнорирования определённых тестов также доступна и для Minitest. Просто используйте `fd_ignore` внутри вашего теста: ```ruby # данный тест не будет отмечен FactoryDoctor it "is ignored" do fd_ignore @user.name = "" refute @user.valid? end ``` ### Использование с Minitest::Reporters Если в вашем проекте вы используете `Minitest::Reporters`, то вам необходимо явно указать это в загрузочном файле (обычно, `test_helper.rb`): ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` **Примечание**: Когда гем `minitest-reporters`у вас установлен глобально, но не объявлен в вашем `Gemfile`, убедитесь, что всегда выполняете команду запуска тестов с `bundle exec` (но мы уверены, что вы и так это делаете). В противном случае вы получите ошибку, вызванную системой плагинов Minitest, которая ищет все файлы в `$LOAD_PATH` для любого `minitest/*_plugin.rb`, таким образом инициализация плагина `minitest-reporters`, который доступен в этом случае, происходит некорректно. ## Настройки Доступны следующие параметры конфигурации (показаны значения по умолчанию): ```ruby TestProf::FactoryDoctor.configure do |config| # Какое событие инструментации отслеживать для трекинга запросов к БД config.event = "sql.active_record" # Игнорировать запросы к БД, которые заняли меньше указанного времени (в секундах) config.threshold = 0.01 end ``` Вы можете использовать соответствующие переменные окружения для указания данных параметров: `FDOC_EVENT` и `FDOC_THRESHOLD` соответственно. --- --- url: /zh-cn/guide/profilers/factory_prof.md --- # Factory 分析器 FactoryProf 追踪你的 factories 的使用统计数据,比如,每个 factory 是怎样被经常使用的。 输出范例: ```sh [TEST PROF INFO] Factories usage Total: 15285 Total top-level: 10286 Total time: 299.5937s Total uniq factories: 119 total top-level total time time per call top-level time name 6091 2715 115.7671s 0.0426s 50.2517s user 2142 2098 93.3152s 0.0444s 92.1915s post ... ``` 它展示了 factory 运行的总数和 *top-level* 运行的数量,比如,不在另一个 factory 运行期间(如在使用关联关系时)。 (**自 v0.9.0 起**)可以展示使用 factories 生成测试数据的耗费时间。 **(自 v0.11.0 起**)可以展示每个 factory 调用的数量。 **注意**:FactoryProf 仅追踪数据库持久化的 factories。对于 FactoryGirl/FactoryBot,是通过使用 `create` 策略所提供的 factories。对于 Fabrication,是使用 `create` 方法所创建的对象。 ## 教学 FactoryProf 可跟 FactoryGirl/FactoryBot 或者 Fabrication 一起使用——应用程序可以同时安装这两个 gem 使用。 使用 `FPROF` 环境变量来激活 FactoryProf: ```sh # Simple profiler FPROF=1 rspec # or FPROF=1 bundle exec rake test ``` ## Factory 火焰图 FactoryProf 最有用的特性就是 *FactoryFlame* 报告。这是对 Brendan Gregg's 的 [火焰图](http://www.brendangregg.com/flamegraphs.html) 的特殊解读,让你可以识别出 *factory cascades*. 要生成 FactoryFlame 报告,把 `FPROF` 环境变量设置为 `flamegraph`: ```sh FPROF=flamegraph rspec # or FPROF=flamegraph bundle exec rake test ``` 报告看起来是这样的: 如何解读它? 每一栏指示一个 *factory stack* 或 *cascade*,其是一个递归 `#create` 方法调用的序列。考虑下面这个范例: ```ruby factory :comment do answer author end factory :answer do question author end factory :question do author end create(:comment) #=> creates 5 records # And the corresponding stack is: # [:comment, :answer, :question, :author, :author, :author] ``` 栏越宽,其堆栈出现的频率越高。 `root` 单元格展示了 `create` 调用的总数。 ## 感谢 * [Martin Spier](https://github.com/spiermar) 的 [d3-flame-graph](https://github.com/spiermar/d3-flame-graph) * [Sam Saffron](https://github.com/SamSaffron) 的 [flame graphs implementation](https://github.com/SamSaffron/flamegraph). --- --- url: /zh-cn/guide/profilers/factory_doctor.md --- # Factory 医生 一个通常的拖慢我们测试的坏模式是不必要的数据库操作。考虑 *下面* 的例子: ```ruby # with FactoryBot/FactoryGirl it "validates name presence" do user = create(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate(:user) user.name = "" expect(user).not_to be_valid end ``` 这里我们创建了一条新的 user 记录,运行所有的回调和校验,并把它保存到数据库。而我们完全不需要这样!下面是一个\_好的\_ 范例: ```ruby # with FactoryBot/FactoryGirl it "validates name presence" do user = build_stubbed(:user) user.name = "" expect(user).not_to be_valid end # with Fabrication it "validates name presence" do user = Fabricate.build(:user) user.name = "" expect(user).not_to be_valid end ``` 查看更多有关 [`build_stubbed`](https://robots.thoughtbot.com/use-factory-girls-build-stubbed-for-a-faster-test) 的内容。 FactoryDoctor 是一个帮你识别这些\_坏\_测试的工具,比如,测试执行了不必要的数据库查询。 输出范例: ```sh [TEST PROF INFO] FactoryDoctor report Total (potentially) bad examples: 2 Total wasted time: 00:13.165 User (./spec/models/user_spec.rb:3) (3 records created, 00:00.628) validates name (./spec/user_spec.rb:8) – 1 record created, 00:00.114 validates email (./spec/user_spec.rb:8) – 2 records created, 00:00.514 ``` **注意**:注意到“potentially” 的字样?不好意思,FactoryDoctor 并非魔术师(其仍在学习中),有时它也会发生“误诊”的事情。 如果你发现了造成 FactoryDoctor 失败的场景,请提交 [issue](https://github.com/test-prof/test-prof/issues)。 你也可以告诉 FactoryDoctor 忽略特定的测试用例/组。只要添加 `:fd_ignore` tag 即可: ```ruby # won't be reported as offense it "is ignored", :fd_ignore do user = create(:user) user.name = "" expect(user).not_to be_valid end ``` ## 教学 FactoryDoctor 支持: * FactoryGirl/FactoryBot * Fabrication (**自 v0.9.0 起**). ### RSpec 使用 `FDOC` 环境变量来激活 FactoryDoctor: ```sh FDOC=1 rspec ... ``` ### 与 RSpecStamp 一起使用 FactoryDoctor 可与 [RSpec Stamp](../recipes/rspec_stamp.md) 一起使用,以自定义 tag 来自动标记 *坏的* 测试用例。例如: ```sh FDOC=1 FDOC_STAMP="fdoc:consider" rspec ... ``` 运行上面命令之后,所有\_潜在的\_ 坏测试用例会都以`fdoc: :consider` tag 标记出来。 ### Minitest 使用 `FDOC` 环境变量来激活 FactoryDoctor: ```sh FDOC=1 ruby ... ``` 或者使用如下 CLI 选项: ```sh ruby ... --factory-doctor ``` 强制 Factory Doctor 忽略特定测试用例的相同选项对于 Minitest 也是可用的。 只要在你的测试用例内使用 `fd_ignore`: ```ruby # won't be reported as offense it "is ignored" do fd_ignore @user.name = "" refute @user.valid? end ``` ### 与 Minitest::Reporters 一起使用 如果在你的项目中正在使用 `Minitest::Reporters`,则必须在测试帮助方法文件中明确声明它: ```sh require 'minitest/reporters' Minitest::Reporters.use! [YOUR_FAVORITE_REPORTERS] ``` **注意**: 当你以 gem 安装了 `minitest-reporters` 但并未在你的 `Gemfile` 里声明时, 确保总是在测试运行命令之前使用 `bundle exec` (但我们相信你总会这样做的)。 否则,你会得到由 Minitest plugin 系统造成的报错, 该系统对所有的 `minitest/*_plugin.rb` 扫描 `$LOAD_PATH` 内的入口, 因此该场景中可用的 `minitest-reporters` plugin 的初始化就不正确了。 ## 配置 > 自 v0.9.0 起 如下配置参数是可用的(展示的为默认情况): ```ruby TestProf::FactoryDoctor.configure do |config| # Which event to track within test example to consider them "DB-dirty" config.event = "sql.active_record" # Consider result "good" if the time in DB is less then the threshold config.threshold = 0.01 end ``` 你也可以使用相应的环境变量: `FDOC_EVENT` 和 `FDOC_THRESHOLD`。 --- --- url: /guide/recipes/factory_all_stub.md --- # FactoryAllStub *Factory All Stub* is a spell to force FactoryBot use only `build_stubbed` strategy (even if you call `create` or `build`). The idea behind it is to quickly fix [Factory Doctor](../profilers/factory_doctor.md) offenses (and even do that automatically). **NOTE**. Only works with FactoryBot. Should be considered only as a temporary specs fix. ## Instructions First, you have to initialize `FactoryAllStub`: ```ruby TestProf::FactoryAllStub.init ``` The initialization process injects custom logic into FactoryBot generator. To enable *all-stub* mode: ```ruby TestProf::FactoryAllStub.enable! ``` To disable *all-stub* mode and use factories as always: ```ruby TestProf::FactoryAllStub.disable! ``` ## RSpec In your `spec_helper.rb` (or `rails_helper.rb` if any): ```ruby require "test_prof/recipes/rspec/factory_all_stub" ``` That would automatically initialize `FactoryAllStub` (no need to call `.init`) and provide `"factory:stub"` shared context with enables it for the marked examples or example groups: ```ruby describe "User" do let(:user) { create(:user) } it "is valid", factory: :stub do # use `build_stubbed` instead of `create` expect(user).to be_valid end end ``` `FactoryAllStub` was designed to be used with `FactoryDoctor` the following way: ```sh # Run FactoryDoctor and mark all offensive examples with factory:stub FDOC=1 FDOC_STAMP=factory:stub rspec ./spec/models ``` --- --- url: /ja/guide/recipes/factory_all_stub.md --- # FactoryAllStub *Factory All Stub* is a spell to force FactoryBot/FactoryGirl use only `build_stubbed` strategy (even if you call `create` or `build`). The idea behind it is to quickly fix [Factory Doctor](../profilers/factory_doctor.md) offenses (and even do that automatically). **NOTE**. Only works with FactoryGirl/FactoryBot. Should be considered only as a temporary specs fix. ## Instructions First, you have to initialize `FactoryAllStub`: ```ruby TestProf::FactoryAllStub.init ``` The initialization process injects custom logic into FactoryBot generator. To enable *all-stub* mode: ```ruby TestProf::FactoryAllStub.enable! ``` To disable *all-stub* mode and use factories as always: ```ruby TestProf::FactoryAllStub.disable! ``` ## RSpec In your `spec_helper.rb` (or `rails_helper.rb` if any): ```ruby require "test_prof/recipes/rspec/factory_all_stub" ``` That would automatically initialize `FactoryAllStub` (no need to call `.init`) and provide `"factory:stub"` shared context with enables it for the marked examples or example groups: ```ruby describe "User" do let(:user) { create(:user) } it "is valid", factory: :stub do # use `build_stubbed` instead of `create` expect(user).to be_valid end end ``` `FactoryAllStub` was designed to be used with `FactoryDoctor` the following way: ```sh # Run FactoryDoctor and mark all offensive examples with factory:stub FDOC=1 FDOC_STAMP=factory:stub rspec ./spec/models ``` --- --- url: /ru/guide/recipes/factory_all_stub.md --- # FactoryAllStub *Factory All Stub* — это *заклинание*, которое заставляет FactoryBot/FactoryGirl использовать стратегию `build_stubbed` даже когда вы вызываете `create` или `build`. Это позволяет одним движением «волшебной палочки» вылечить тесты, которые [Factory Doctor](../profilers/factory_doctor.md) считает «больными». ## Инструкция Сначала необходимо явно активировать `FactoryAllStub`: ```ruby TestProf::FactoryAllStub.init ``` В процессе инициализации TestProf *встраивается* в логику генератора FactoryBot. После этого необходимо также явно включить *all-stub* режим: ```ruby TestProf::FactoryAllStub.enable! ``` Для отключения *all-stub* режима выполните: ```ruby TestProf::FactoryAllStub.enable! ``` ## Использование с RSpec Добавьте в `spec_helper.rb` (или `rails_helper.rb`): ```ruby require "test_prof/recipes/rspec/factory_all_stub" ``` Это автоматически активирует `FactoryAllStub` (не нужно делать `.init`) и добавить shared context `"factory:stub"`, который вы сможете подключать вручную или с помощью тега `factory: :stub` для включения режима *all-stub*: ```ruby describe "User" do let(:user) { create(:user) } it "is valid", factory: :stub do # будет использована стратегия `build_stubbed` вместо `create` expect(user).to be_valid end end ``` **Бонус**: Для автоматического исправления тестов, обнаруженных `FactoryDoctor`, выполните следующую команду: ```sh FDOC=1 FDOC_STAMP=factory:stub rspec ./spec/models ``` --- --- url: /zh-cn/guide/recipes/factory_all_stub.md --- # FactoryAllStub *Factory All Stub* 是一个强制 FactoryBot/FactoryGirl 仅使用 `build_stubbed` 策略的说法(即使你调用了 `create` 或 `build`)。 背后的思想是快速修复 [Factory Doctor](../profilers/factory_doctor.md) 的问题(并甚至自动执行该操作)。 **注意**,仅支持 FactoryGirl/FactoryBot。应该仅被视为一种 specs 的临时修复。 ## 教学 首先,你必须初始化 `FactoryAllStub`: ```ruby TestProf::FactoryAllStub.init ``` 初始化过程注入自定义逻辑到 FactoryBot 生成器内部。 启用 *all-stub* 模式: ```ruby TestProf::FactoryAllStub.enable! ``` 禁用 *all-stub* 模式并总是使用 factories: ```ruby TestProf::FactoryAllStub.enable! ``` ## RSpec 在 `spec_helper.rb` 中(或者 `rails_helper.rb` 中,如果有的话): ```ruby require "test_prof/recipes/rspec/factory_all_stub" ``` 这会自动初始化 `FactoryAllStub` (无需调用 `.init`)并提供 `"factory:stub"` 的 shared context,用来对所标记的测试用例或测试组启用: ```ruby describe "User" do let(:user) { create(:user) } it "is valid", factory: :stub do # use `build_stubbed` instead of `create` expect(user).to be_valid end end ``` `FactoryAllStub` 被设计跟 `FactoryDoctor` 以如下方式一起使用: ```sh # Run FactoryDoctor and mark all offensive examples with factory:stub FDOC=1 FDOC_STAMP=factory:stub rspec ./spec/models ``` --- --- url: /guide/recipes/factory_default.md --- # FactoryDefault *FactoryDefault* aims to help you cope with *factory cascades* (see [FactoryProf](../profilers/factory_prof.md)) by reusing associated records. It can be very useful when you're working on a typical SaaS application (or other hierarchical data). Consider an example. Assume we have the following factories: ```ruby factory :account do end factory :user do account end factory :project do account user end factory :task do account project user end ``` Or in case of Fabrication: ```ruby Fabricator(:account) do end Fabricator(:user) do account end # etc. ``` And we want to test the `Task` model: ```ruby describe "PATCH #update" do let(:task) { create(:task) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end # ... end ``` How many users and accounts are created per example? Two and four respectively. And it breaks our logic (every object should belong to the same account). Typical workaround: ```ruby describe "PATCH #update" do let(:account) { create(:account) } let(:project) { create(:project, account: account) } let(:task) { create(:task, project: project, account: account) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` That works. And there are some cons: it's a little bit verbose and error-prone (easy to forget something). Here is how we can deal with it using FactoryDefault: ```ruby describe "PATCH #update" do let(:account) { create_default(:account) } let(:project) { create_default(:project) } let(:task) { create(:task) } # and if we need more projects, users, tasks with the same parent record, # we just write let(:another_project) { create(:project) } # uses the same account let(:another_task) { create(:task) } # uses the same account it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` **NOTE**. This feature introduces a bit of *magic* to your tests, so use it with caution ('cause tests should be human-readable first). Good idea is to use defaults for top-level entities only (such as tenants in multi-tenancy apps). ## Instructions In your `spec_helper.rb`: ```ruby require "test_prof/recipes/rspec/factory_default" ``` This adds the following methods to FactoryBot and/or Fabrication: * `FactoryBot#set_factory_default(factory, object)` / `Fabricate.set_fabricate_default(factory, object)` – use the `object` as default for associations built with `factory`. Example: ```ruby let(:user) { create(:user) } before { FactoryBot.set_factory_default(:user, user) } # You can also set the default factory with traits FactoryBot.set_factory_default([:user, :admin], admin) # Or (since v1.4) FactoryBot.set_factory_default(:user, :admin, admin) # You can also register a default record for specific attribute overrides Fabricate.set_fabricate_default(:post, post, state: "draft") ``` * `FactoryBot#create_default(...)` / `Fabricate.create_default(...)` – is a shortcut for `create` + `set_factory_default`. * `FactoryBot#get_factory_default(factory)` / `Fabricate.get_fabricate_default(factory)` – retrieves the default value for `factory` (since v1.4). ```rb # This method also supports traits admin = FactoryBot.get_factory_default(:user, :admin) ``` **IMPORTANT:** Defaults are **cleaned up after each example** by default (i.e., when using `test_prof/recipes/rspec/factory_default`). ### Using with `before_all` / `let_it_be` Defaults created within `before_all` and `let_it_be` are not reset after each example, but only at the end of the corresponding example group. So, it's possible to call `create_default` within `let_it_be` without any additional configuration. **RSpec only** **IMPORTANT:** You must load FactoryDefault after loading BeforeAll to make this feature work. **NOTE**. Regular `before(:all)` callbacks are not supported. ### Working with traits You can use traits in your associations, for example: ```ruby factory :comment do user end factory :post do association :user, factory: %i[user able_to_post] end factory :view do association :user, factory: %i[user unable_to_post_only_view] end ``` If there is a default value for the `user` factory, it's gonna be used independently of traits. This may break your logic. To prevent this, configure FactoryDefault to preserve traits: ```ruby # Globally TestProf::FactoryDefault.configure do |config| config.preserve_traits = true end # or in-place create_default(:user, preserve_traits: true) ``` Creating a default with trait works as follows: ```ruby # Create a default with trait user = create_default(:user_poster, :able_to_post) # When an association has no traits specified, the default with trait is used create(:comment).user == user #=> true # When an association has the matching trait specified, the default is used, too create(:post).user == user #=> true # When the association's trait differs, default is skipped create(:view).user == user #=> false ``` ### Handling attribute overrides It's possible to define attribute overrides for associations: ```ruby factory :post do association :user, name: "Poster" end factory :view do association :user, name: "Viewer" end ``` FactoryDefault ignores such overrides and still returns a default `user` record (if created). You can turn the attribute awareness feature on to skip the default record if overrides don't match the default object attributes: ```ruby # Globally TestProf::FactoryDefault.configure do |config| config.preserve_attributes = true end # or in-place create_default :user, preserve_attributes: true ``` **NOTE:** In the future versions of Test Prof, both `preserve_traits` and `preserve_attributes` will default to true. We recommend settings them to true if you just starting using this feature. ### Ignoring default factories You can temporary disable the defaults usage by wrapping a code with the `skip_factory_default` method: ```ruby account = create_default(:account) another_account = skip_factory_default { create(:account) } expect(another_account).not_to eq(account) ``` ### Showing usage stats You can display the FactoryDefault usage stats by setting the `FACTORY_DEFAULT_SUMMARY=1` or `FACTORY_DEFAULT_STATS=1` env vars or by setting the configuration values: ```ruby TestProf::FactoryDefault.configure do |config| config.report_summary = true # Report stats prints the detailed usage information (including summary) config.report_stats = true end ``` For example: ```sh $ FACTORY_DEFAULT_SUMMARY=1 bundle exec rspec FactoryDefault summary: hit=11 miss=3 ``` Where `hit` indicates the number of times the default factory value was used instead of a new one when an association was created; `miss` indicates the number of time the default value was ignored due to traits or attributes mismatch. ## Factory Default profiling, or when to use defaults Factory Default ships with the profiler, which can help you to see how associations are being used in your test suite, so you can decide on using `create_default` or not. To enable profiling, run your tests with the `FACTORY_DEFAULT_PROF=1` set: ```sh $ FACTORY_DEFAULT_PROF=1 bundle exec rspec spec/some/file_spec.rb ..... [TEST PROF INFO] Factory associations usage: factory count total time user 17 00:42.010 user[traited] 15 00:31.560 user{tag:"some tag"} 1 00:00.205 Total associations created: 33 Total uniq associations created: 3 Total time spent: 01:13.775 ``` Since default factories are usually registered per an example group (or test class), we recommend running this profiler against a particular file, so you can quickly identify the possibility of adding `create_default` and improve the tests speed. **NOTE:** You can also use the profiler to measure the effect of adding `create_default`; for that, compare the results of running the profiler with FactoryDefault enabled and disabled (you can do that by passing the `FACTORY_DEFAULT_DISABLED=1` env var). --- --- url: /ja/guide/recipes/factory_default.md --- # FactoryDefault *FactoryDefault* aims to help you cope with *factory cascades* (see [FactoryProf](../profilers/factory_prof.md)) by reusing associated records. **NOTE**. Only works with FactoryGirl/FactoryBot. It can be very useful when you're working on a typical SaaS application (or other hierarchical data). Consider an example. Assume we have the following factories: ```ruby factory :account do end factory :user do account end factory :project do account user end factory :task do account project user end ``` And we want to test the `Task` model: ```ruby describe "PATCH #update" do let(:task) { create(:task) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end # ... end ``` How many users and accounts are created per example? Two and four respectively. And it breaks our logic (every object should belong to the same account). Typical workaround: ```ruby describe "PATCH #update" do let(:account) { create(:account) } let(:project) { create(:project, account: account) } let(:task) { create(:task, project: project, account: account) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` That works. And there are some cons: it's a little bit verbose and error-prone (easy to forget something). Here is how we can deal with it using FactoryDefault: ```ruby describe "PATCH #update" do let(:account) { create_default(:account) } let(:project) { create_default(:project) } let(:task) { create(:task) } # and if we need more projects, users, tasks with the same parent record, # we just write let(:another_project) { create(:project) } # uses the same account let(:another_task) { create(:task) } # uses the same account it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` **NOTE**. This feature introduces a bit of *magic* to your tests, so use it with caution ('cause tests should be human-readable first). Good idea is to use defaults for top-level entities only (such as tenants in multi-tenancy apps). ## Instructions In your `spec_helper.rb`: ```ruby require "test_prof/recipes/rspec/factory_default" ``` This adds two new methods to FactoryBot: * `FactoryBot#set_factory_default(factory, object)` – use the `object` as default for associations built with `factory` Example: ```ruby let(:user) { create(:user) } before { FactoryBot.set_factory_default(:user, user) } ``` * `FactoryBot#create_default(factory, *args)` – is a shortcut for `create` + `set_factory_default`. **IMPORTANT:** Defaults are **cleaned up after each example** by default (i.e., when using `test_prof/recipes/rspec/factory_default`). ### Using with `before_all` / `let_it_be` Defaults created within `before_all` and `let_it_be` are not reset after each example, but only at the end of the corresponding example group. So, it's possible to call `create_default` within `let_it_be` without any additional configuration. **RSpec only** **IMPORTANT:** You must load FactoryDefault after loading BeforeAll to make this feature work. **NOTE**. Regular `before(:all)` callbacks are not supported. ### Working with traits You can use traits in your associations, for example: ```ruby factory :comment do user end factory :post do association :user, factory: %i[user able_to_post] end factory :view do association :user, factory: %i[user unable_to_post_only_view] end ``` If there is a default value for the `user` factory, it's gonna be used independently of traits. This may break your logic. To prevent this, configure FactoryDefault to preserve traits: ```ruby # Globally TestProf::FactoryDefault.configure do |config| config.preserve_traits = true end # or in-place create_default(:user, preserve_traits: true) ``` Creating a default with trait works as follows: ```ruby # Create a default with trait user = create_default(:user_poster, :able_to_post) # When an association has no traits specified, the default with trait is used create(:comment).user == user #=> true # When an association has the matching trait specified, the default is used, too create(:post).user == user #=> true # When the association's trait differs, default is skipped create(:view).user == user #=> false ``` ### Handling attribute overrides It's possible to define attribute overrides for associations: ```ruby factory :post do association :user, name: "Poster" end factory :view do association :user, name: "Viewer" end ``` FactoryDefault ignores such overrides and still returns a default `user` record (if created). You can turn the attribute awareness feature on to skip the default record if overrides don't match the default object attributes: ```ruby # Globally TestProf::FactoryDefault.configure do |config| config.preserve_attributes = true end # or in-place create_default :user, preserve_attributes: true ``` **NOTE:** In the future versions of Test Prof, both `preserve_traits` and `preserve_attributes` will default to true. We recommend settings them to true if you just starting using this feature. ### Ignoring default factories You can temporary disable the defaults usage by wrapping a code with the `skip_factory_default` method: ```ruby account = create_default(:account) another_account = skip_factory_default { create(:account) } expect(another_account).not_to eq(account) ``` ### Showing usage stats You can display the FactoryDefault usage stats by setting the `FACTORY_DEFAULT_SUMMARY=1` or `FACTORY_DEFAULT_STATS=1` env vars or by setting the configuration values: ```ruby TestProf::FactoryDefault.configure do |config| config.report_summary = true # Report stats prints the detailed usage information (including summary) config.report_stats = true end ``` For example: ```sh $ FACTORY_DEFAULT_SUMMARY=1 bundle exec rspec FactoryDefault summary: hit=11 miss=3 ``` Where `hit` indicates the number of times the default factory value was used instead of a new one when an association was created; `miss` indicates the number of time the default value was ignored due to traits or attributes mismatch. ## Factory Default profiling, or when to use defaults Factory Default ships with the profiler, which can help you to see how associations are being used in your test suite, so you can decide on using `create_default` or not. To enable profiling, run your tests with the `FACTORY_DEFAULT_PROF=1` set: ```sh $ FACTORY_DEFAULT_PROF=1 bundle exec rspec spec/some/file_spec.rb ..... [TEST PROF INFO] Factory associations usage: factory count total time user 17 00:42.010 user[traited] 15 00:31.560 user{tag:"some tag"} 1 00:00.205 Total associations created: 33 Total uniq associations created: 3 Total time spent: 01:13.775 ``` Since default factories are usually registered per an example group (or test class), we recommend running this profiler against a particular file, so you can quickly identify the possibility of adding `create_default` and improve the tests speed. **NOTE:** You can also use the profiler to measure the effect of adding `create_default`; for that, compare the results of running the profiler with FactoryDefault enabled and disabled (you can do that by passing the `FACTORY_DEFAULT_DISABLED=1` env var). --- --- url: /ru/guide/recipes/factory_default.md --- # FactoryDefault *FactoryDefault* — это механизм, который помогает бороться с *каскадами фабрик* (см. [FactoryProf](../profilers/factory_prof.md)) путём переиспользования ассоциированных объектов. **Примечание**: Поддерживается только FactoryGirl/FactoryBot. Рассмотрим пример типичного SaaS-приложения с денормализацией по аккаунту. Допустим, у вас есть следующие фабрики: ```ruby factory :account do end factory :user do account end factory :project do account user end factory :task do account project user end ``` и следующий тест модели `Task`: ```ruby describe "PATCH #update" do let(:task) { create(:task) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end # ... end ``` Сколько пользователей (*users*) и аккаунтов (*accounts*) будет создано при выполнении теста? Два и четыре соответственно. Однако это противоречит логике: все объекты должны относится к одному аккаунту! Мы могли бы это исправить так: ```ruby describe "PATCH #update" do let(:account) { create(:account) } let(:project) { create(:project, account: account) } let(:task) { create(:task, project: project, account: account) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` Это будет работать. Но у данного решения есть свои недостатки: больше кода и больше вероятность ошибиться (забыть указать аккаунт для какого-то объекта). С помощью FactoryDefault мы можем решить это так: ```ruby describe "PATCH #update" do let(:account) { create_default(:account) } let(:project) { create_default(:project) } let(:task) { create(:task) } # Если нам нужны другие объекты, использующие ассоциации account/project, # мы пишем (в обоих случаях будет использован аккаунт, объявленный в начале группы тестов) let(:another_project) { create(:project) } let(:another_task) { create(:task, project: another_project) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` **Примечание**: данный инструмент добавляет немного *магии* в тесты и должен быть использован с осторожностью. Хорошая идея – использовать значения по умолчанию только для *глобальных* сущностей. ## Инструкция В вашем `spec_helper.rb`: ```ruby require "test_prof/recipes/rspec/factory_default" ``` После загрузки будет добавлено два новых метода для FactoryBot: * `FactoryBot#set_factory_default(factory, object)` – устанавливает `object` объектом по умолчанию для ассоциаций с именем `factory`. Например: ```ruby let(:user) { create(:user) } before { FactoryBot.set_factory_default(:user, user) } ``` * `FactoryBot#create_default(factory, *args)` – эквивалентно `create` + `set_factory_default`. **Примечание**: Значения по умолчанию **автоматически сбрасываются после каждого теста**, что не будет работать с `before(:all)` / [`before_all`](./before_all.md) / [`let_it_be`](./let_it_be.md). Смотрите возможный вариант решения этой проблемы [здесь](https://github.com/test-prof/test-prof/issues/125#issuecomment-471706752). ### Использование с FactoryBot traits Допустим, что у вас есть следующие фабрики: ```ruby factory :post do association :user, factory: %i[user able_to_post] end factory :view do association :user, factory: %i[user unable_to_post_only_view] end ``` и вы устанавливаете значение по умолчанию для фабрики `user` — оно будет использовано для обеих фабрик, несмотря на то, что используются разные *трейты*. Вы можете воспользоваться опцией `FactoryDefault.preserve_traits = true` или указать для конкретного объекта `create_default(:user, preserve_traits: true)`. В этом случае значение по умолчанию будет использовано только для ассоциаций **без трейтов**. --- --- url: /zh-cn/guide/recipes/factory_default.md --- # FactoryDefault *Factory Default* 旨在通过重用关联关系数据记录帮你对付 *factory cascades*(参看 [FactoryProf](../profilers/factory_prof.md))。 **注意**,仅可用于 FactoryGirl/FactoryBot。 当你在处理典型的 SaaS 应用(或其他分等级的数据)时,它会很有用。 考虑一个例子。假设你有如下的 factories: ```ruby factory :account do end factory :user do account end factory :project do account user end factory :task do account project user end ``` 而我们想要测试 `Task` model: ```ruby describe "PATCH #update" do let(:task) { create(:task) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end # ... end ``` 每个测试用例会创建多少个 users 和 accounts? 分别是两个和四个。 而且它破坏了我们的逻辑(每个对象应该属于同一个 account)。 典型的变通办法是: ```ruby describe "PATCH #update" do let(:account) { create(:account) } let(:project) { create(:project, account: account) } let(:task) { create(:task, project: project, account: account) } it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` 这是能工作。然而有一些缺点:有点太啰嗦了,且有错误的倾向(容易忘记某些东西)。 下面是我们如何使用 FactoryDefault 来处理它: ```ruby describe "PATCH #update" do let(:account) { create_default(:account) } let(:project) { create_default(:project) } let(:task) { create(:task) } # and if we need more projects, users, tasks with the same parent record, # we just write let(:another_project) { create(:project) } # uses the same account let(:another_task) { create(:task) } # uses the same account it "works" do patch :update, id: task.id, task: {completed: "t"} expect(response).to be_success end end ``` **注意**,该特性引入了一些\_魔法\_到你的测试中,请谨慎使用。(因为测试应该首先要易于人们阅读)。好的做法是仅用于 top-level(比如多租户中 App 中的租户)。 ## 教学 在 `spec_helper.rb` 中: ```ruby require "test_prof/recipes/rspec/factory_default" ``` 这为 FactoryBot 添加了两个新方法: * `FactoryBot#set_factory_default(factory, object)`——对于 `factory` 构建的关联关系,使用 `object` 作为默认值。 范例: ```ruby let(:user) { create(:user) } before { FactoryBot.set_factory_default(:user, user) } ``` * `FactoryBot#create_default(factory, *args)` —— `create` + `set_factory_default`的快捷方式。 **注意**,Defaults 默认是**在每个测试用例后被清除掉**。这意味着你在 `before(:all)` / [`before_all`](./before_all.md) / [`let_it_be`](./let_it_be.md) 定义的内部不能创建默认值。这在未来可能会改变,目前 [请查看这个变通办法](https://github.com/test-prof/test-prof/issues/125#issuecomment-471706752)。 ### 与 traits 一起使用 当你在关联关系中有类似这样的 traits 时: ```ruby factory :post do association :user, factory: %i[user able_to_post] end factory :view do association :user, factory: %i[user unable_to_post_only_view] end ``` 并为 `user` factory 设置了默认值——那么你会发现同样的对象被用于上面所有 factories 中。有时候这会破坏你的逻辑。 要防止这个——请设置 `FactoryDefault.preserve_traits = true` 或者使用 per-factory 来覆盖 `create_default(:user, preserve_traits: true)`。 这会针对有明确定义的 traits 的关联关系恢复到初始的 FactoryBot 行为。 --- --- url: /guide/profilers/factory_prof.md --- # FactoryProf FactoryProf tracks your factories usage statistics, i.e. how often each factory has been used. Example output: ```sh [TEST PROF INFO] Factories usage Total: 15285 Total top-level: 10286 Total time: 04:31.222 (out of 07.16.124) Total uniq factories: 119 name total top-level total time time per call top-level time user 6091 2715 115.7671s 0.0426s 50.2517s post 2142 2098 93.3152s 0.0444s 92.1915s ... ``` It shows both the total number of the factory runs and the number of *top-level* runs, i.e. not during another factory invocation (e.g. when using associations.) It also shows the time spent generating records with factories and the amount of time taken per factory call. **NOTE**: FactoryProf only tracks the database-persisted factories. In case of FactoryBot these are the factories provided by using `create` strategy. In case of Fabrication - objects that created using `create` method. ## Instructions FactoryProf can be used with FactoryBot or Fabrication - application can be bundled with both gems at the same time. To activate FactoryProf use `FPROF` environment variable: ```sh # Simple profiler FPROF=1 rspec # or FPROF=1 bundle exec rake test ``` ### [*Nate Heckler*](https://twitter.com/nateberkopec/status/1389945187766456333) mode To encourage you to fix your factories as soon as possible, we also have a special *Nate heckler* mode. Drop this into your `rails_helper.rb` or `test_helper.rb`: ```ruby require "test_prof/factory_prof/nate_heckler" ``` And for every test run see the overall factories usage: ```sh [TEST PROF INFO] Time spent in factories: 04:31.222 (54% of total time) ``` ### Variations You can also add *variations* (such as traits, overrides) information to reports by providing the `FPROF_VARS=1` environment variable or enabling it in your code: ```ruby TestProf::FactoryProf.configure do |config| config.include_variations = true end ``` For example: ```sh $ FPROF=1 FPROF_VARS=1 bin/rails test ... [TEST PROF INFO] Factories usage Total: 15285 Total top-level: 10286 Total time: 04:31.222 (out of 07.16.124) Total uniq factories: 119 name total top-level total time time per call top-level time user 6091 2715 115.7671s 0.0426s 50.251s - 5243 1989 84.231s 0.0412s 34.321s .admin 823 715 15.767s 0.0466s 5.257s [name,role] 25 11 7.671s 0.0666s 1.257s post 2142 2098 93.315s 0.0444s 92.191s _ 2130 2086 87.685s 0.0412s 88.191s .draft[tags] 12 12 9.315s 0.164s 42.115s ... ``` In the example above, `-` indicates a factory without traits or overrides (e.g., `create(:user)`), `.xxx` indicates a trait and `[a,b]` indicates the overrides keys, e.g., `create(:user, :admin)` is an `.admin` variation, while `create(:post, :draft, tags: ["a"])`—`.draft[tags]` #### Variations limit config When running FactoryProf, the output may contain a variant that is too long, which will distort the output. To avoid this and focus on the most important statistics you can specify a variations limit value. Then a special ID (`[...]`) will be shown instead of the variant with the number of traits/overrides exceeding the limit. To use variations limit parameter set `FPROF_VARIATIONS_LIMIT` environment variable to `N` (where `N` is a limit number): ```sh FPROF=1 FPROF_VARIATIONS_LIMIT=5 rspec # or FPROF=1 FPROF_VARIATIONS_LIMIT=5 bundle exec rake test ``` Or you can set the limit parameter through the `FactoryProf` configuration: ```ruby TestProf::FactoryProf.configure do |config| config.variations_limit = 5 end ``` ### Exporting profile results to a JSON file FactoryProf can save profile results as a JSON file. To use this feature, set the `FPROF` environment variable to `json`: ```sh FPROF=json rspec # or FPROF=json bundle exec rake test ``` Example output: ```sh [TEST PROF INFO] Profile results to JSON: tmp/test_prof/test-prof.result.json ``` ### Reducing the output When running FactoryProf, the output may contain a lot of lines for factories that has been used a few times. To avoid this and focus on the most important statistics you can specify a threshold value. Then you will be shown the factories whose total number exceeds the threshold. To use threshold option set `FPROF_THRESHOLD` environment variable to `N` (where `N` is a threshold number): ```sh FPROF=1 FPROF_THRESHOLD=30 rspec # or FPROF=1 FPROF_THRESHOLD=30 bundle exec rake test ``` Or you can set the threshold parameter through the `FactoryProf` configuration: ```ruby TestProf::FactoryProf.configure do |config| config.threshold = 30 end ``` ### Truncate long factory-names When running FactoryProf on a codebase with long factory names, the table layout may break. To avoid this you can allow FactoryProf to truncate these names. To use truncation set `FPROF_TRUNCATE_NAMES` environment variable to `1`: ```sh FPROF=1 FPROF_TRUNCATE_NAMES=1 rspec # or FPROF=1 FPROF_TRUNCATE_NAMES=1 bundle exec rake test ``` Or you can set the truncate\_names parameter through the `FactoryProf` configuration: ```ruby TestProf::FactoryProf.configure do |config| config.truncate_names = true end ``` ## Factory Flamegraph The most useful feature of FactoryProf is the *FactoryFlame* report. That's the special interpretation of Brendan Gregg's [flame graphs](http://www.brendangregg.com/flamegraphs.html) which allows you to identify *factory cascades*. To generate FactoryFlame report set `FPROF` environment variable to `flamegraph`: ```sh FPROF=flamegraph rspec # or FPROF=flamegraph bundle exec rake test ``` That's how a report looks like: How to read this? Every column represents a *factory stack* or *cascade*, that is a sequence of recursive `#create` method calls. Consider an example: ```ruby factory :comment do answer author end factory :answer do question author end factory :question do author end create(:comment) #=> creates 5 records # And the corresponding stack is: # [:comment, :answer, :question, :author, :author, :author] ``` The wider column the more often this stack appears. The `root` cell shows the total number of `create` calls. ## Acknowledgments * Thanks to [Martin Spier](https://github.com/spiermar) for [d3-flame-graph](https://github.com/spiermar/d3-flame-graph) * Thanks to [Sam Saffron](https://github.com/SamSaffron) for his [flame graphs implementation](https://github.com/SamSaffron/flamegraph). --- --- url: /ja/guide/profilers/factory_prof.md --- # FactoryProf FactoryProf tracks your factories usage statistics, i.e. how often each factory has been used. Example output: ```sh [TEST PROF INFO] Factories usage Total: 15285 Total top-level: 10286 Total time: 04:31.222 (out of 07.16.124) Total uniq factories: 119 total top-level total time time per call top-level time name 6091 2715 115.7671s 0.0426s 50.2517s user 2142 2098 93.3152s 0.0444s 92.1915s post ... ``` It shows both the total number of the factory runs and the number of *top-level* runs, i.e. not during another factory invocation (e.g. when using associations.) It also shows the time spent generating records with factories and the amount of time taken per factory call. **NOTE**: FactoryProf only tracks the database-persisted factories. In case of FactoryGirl/FactoryBot these are the factories provided by using `create` strategy. In case of Fabrication - objects that created using `create` method. ## Instructions FactoryProf can be used with FactoryGirl/FactoryBot or Fabrication - application can be bundled with both gems at the same time. To activate FactoryProf use `FPROF` environment variable: ```sh # Simple profiler FPROF=1 rspec # or FPROF=1 bundle exec rake test ``` ### [*Nate Heckler*](https://twitter.com/nateberkopec/status/1389945187766456333) mode To encourage you to fix your factories as soon as possible, we also have a special *Nate heckler* mode. Drop this into your `rails_helper.rb` or `test_helper.rb`: ```ruby require "test_prof/factory_prof/nate_heckler" ``` And for every test run see the overall factories usage: ```sh [TEST PROF INFO] Time spent in factories: 04:31.222 (54% of total time) ``` ### Exporting profile results to a JSON file FactoryProf can save profile results as a JSON file. To use this feature, set the `FPROF` environment variable to `json`: ```sh FPROF=json rspec # or FPROF=json bundle exec rake test ``` Example output: ``` [TEST PROF INFO] Profile results to JSON: tmp/test_prof/test-prof.result.json ``` ## Factory Flamegraph The most useful feature of FactoryProf is the *FactoryFlame* report. That's the special interpretation of Brendan Gregg's [flame graphs](http://www.brendangregg.com/flamegraphs.html) which allows you to identify *factory cascades*. To generate FactoryFlame report set `FPROF` environment variable to `flamegraph`: ```sh FPROF=flamegraph rspec # or FPROF=flamegraph bundle exec rake test ``` That's how a report looks like: ![](/assets/factory-flame.gif) How to read this? Every column represents a *factory stack* or *cascade*, that is a sequence of recursive `#create` method calls. Consider an example: ```ruby factory :comment do answer author end factory :answer do question author end factory :question do author end create(:comment) #=> creates 5 records # And the corresponding stack is: # [:comment, :answer, :question, :author, :author, :author] ``` The wider column the more often this stack appears. The `root` cell shows the total number of `create` calls. ## Acknowledgments * Thanks to [Martin Spier](https://github.com/spiermar) for [d3-flame-graph](https://github.com/spiermar/d3-flame-graph) * Thanks to [Sam Saffron](https://github.com/SamSaffron) for his [flame graphs implementation](https://github.com/SamSaffron/flamegraph). --- --- url: /ru/guide/profilers/factory_prof.md --- # FactoryProf FactoryProf помогает вам анализировать использование фабрик в тестах: как часто и каким образом создаются те или иные объекты. Это помогает находить *каскады фабрик* — одну из самых главных причин медленных тестов. > Подробнее о каскадах фабрик читайте в статье [TestProf II: Factory therapy for your Ruby tests](https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest). Пример отчёта FactoryProf: ```sh [TEST PROF INFO] Factories usage Total: 15285 Total top-level: 10286 Total time: 299.5937s Total uniq factories: 119 total top-level total time time per call top-level time name 6091 2715 115.7671s 0.0426s 50.2517s user 2142 2098 93.3152s 0.0444s 92.1915s post ... ``` В данном отчёте отображается как общее число использования фабрики, так и отдельно число верхнеуровневых вызовов, т.е. без учёта вызовов внутри других фабрик (например, при создании ассоциированных объектов). **NOTE**: FactoryProf учитывает только объекты, сохраняемые в базе данных, то есть объекты, созданные с помощью метода `#create` FactoryGirl/FactoryBot или Fabrication. ## Инструкция FactoryProf работает как с FactoryGirl/FactoryBot, так и с Fabrication. Для запуска профилирования используйте переменную окружения `FPROF`: ```sh FPROF=1 rspec # или FPROF=1 bundle exec rake test ``` ### Режим [*задиры Нейта*](https://twitter.com/nateberkopec/status/1389945187766456333) Чтобы вы не забывали о том, сколько времени *съедают* ваши фабрики, мы предлагаем вам использовать специальный режим работы Factory Prof, придуманный [Нейтом Беркопеком](https://twitter.com/nateberkopec). Добавьте в ваш `rails_helper.rb` или `test_helper.rb` следующую строчку: ```ruby require "test_prof/factory_prof/nate_heckler" ``` Теперь в конце каждого запуска тестов вы будете видеть, сколько времени вы потратили на создание данных фабриками: ```sh [TEST PROF INFO] Time spent in factories: 04:31.222 (54% of total time) ``` ## Флеймграфы для фабрик Наиболее полезным форматом отчёта FactoryProf является так называемый *FactoryFlame* отчёт. Данный формат является адаптацией оригинальной идеи *флеймграфов*, [представленной Брэндоном Греггом](http://www.brendangregg.com/flamegraphs.html). Данный формат помогает **находить каскады фабрик**. Для генерации FactoryFlame отчёте укажите `flamegraph` в качестве значение переменной `FPROF`: ```sh FPROF=flamegraph rspec # или FPROF=flamegraph bundle exec rake test ``` В результате вы получите ссылку на HTML-файл с интерактивным графиком: Давайте разберёмся, какую информацию мы можем из него получить? Каждая колонка представляет собой *стеб фабрик* или *каскад* — он начинается (снизу) с имени фабрики, которая была вызвана в тестовом коде, и продолжается цепочкой вложенных вызовов метода `#create`. Рассмотрим пример: ```ruby factory :comment do answer author end factory :answer do question author end factory :question do author end ``` Допустим, мы хотим создать комментарий: ```ruby create(:comment) #=> создаёт 5 объектов ``` Соответствующий стек фабрик выглядит следующим образом: ```ruby [:comment, :answer, :question, :author, :author, :author] ``` Вернёмся к графику. Ширина колонки соответствует *популярности* данного стека: чем шире, тем чаще он встречается. Ячейка `root` показывает общее число вызовов метода `#create`. Для оптимизации времени выполнения тестов вам нужно стремиться избавиться от одновременно *широких* и *высоких* стеков на графике. --- --- url: /guide/getting_started.md --- # Getting Started ## Installation Add `test-prof` gem to your application: ```ruby group :test do gem "test-prof", "~> 1.0" end ``` That's it! Now you can use TestProf [profilers](/#profilers). ## Configuration TestProf global configuration is used by most of the profilers: ```ruby TestProf.configure do |config| # the directory to put artifacts (reports) in ('tmp/test_prof' by default) config.output_dir = "tmp/test_prof" # use unique filenames for reports (by simply appending current timestamp) config.timestamps = true # color output config.color = true # where to write logs (defaults) config.output = $stdout # alternatively, you can specify a custom logger instance config.logger = MyLogger.new end ``` You can also dynamically add artifacts/reports suffixes via `TEST_PROF_REPORT` env variable. It is useful if you're not using timestamps and want to generate multiple reports with different setups and compare them. For example, let's compare tests load time with and without `bootsnap` using [`stackprof`](./profilers/stack_prof.md): ```sh # Generate first report using `-with-bootsnap` suffix $ TEST_STACK_PROF=boot TEST_PROF_REPORT=with-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-with-bootsnap.dump # Assume that you disabled bootsnap and want to generate a new report $ TEST_STACK_PROF=boot TEST_PROF_REPORT=no-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-no-bootsnap.dump ``` Now you have two stackprof reports with clear names! --- --- url: /guide/recipes/let_it_be.md --- # Let It Be Let's bring a little bit of magic and introduce a new way to set up a *shared* test data. Suppose you have the following setup: ```ruby describe BeatleWeightedSearchQuery do let!(:paul) { create(:beatle, name: "Paul") } let!(:ringo) { create(:beatle, name: "Ringo") } let!(:george) { create(:beatle, name: "George") } let!(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # and more examples here end ``` We don't need to re-create the Fab Four for every example, do we? We already have [`before_all`](./before_all.md) to solve the problem of *repeatable* data: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end specify { expect(subject.call("joh")).to contain_exactly(@john) } # ... end ``` That technique works pretty good but requires us to use instance variables and define everything at once. Thus it's not easy to refactor existing tests which use `let/let!` instead. With `let_it_be` you can do the following: ```ruby describe BeatleWeightedSearchQuery do let_it_be(:paul) { create(:beatle, name: "Paul") } let_it_be(:ringo) { create(:beatle, name: "Ringo") } let_it_be(:george) { create(:beatle, name: "George") } let_it_be(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # and more examples here end ``` That's it! Just replace `let!` with `let_it_be`. That's equal to the `before_all` approach but requires less refactoring. **NOTE**: Great superpower that `before_all` provides comes with a great responsibility. Make sure to check the [Caveats section](#caveats) of this document for details. ## Instructions In your `rails_helper.rb` or `spec_helper.rb`: ```ruby require "test_prof/recipes/rspec/let_it_be" ``` In your tests: ```ruby describe MySuperDryService do let_it_be(:user) { create(:user) } # ... end ``` `let_it_be` won't automatically bring the database to its previous state between the examples, it only does that between example groups. Use Rails' native `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1), RSpec Rails' `use_transactional_fixtures`, DatabaseCleaner, or custom code that begins a transaction before each test and rolls it back after. ## Caveats ### Database is rolled back to a pristine state, but the objects are not If you modify objects generated within a `let_it_be` block in your examples, you maybe have to re-initiate them. We have a built-in *modifiers* support for that. ### Database is not rolled back between tests Database is not rolled back between RSpec examples, only between example groups. We don't want to reinvent the wheel and encourage you to use other tools that provide this out of the box. If you're using RSpec Rails, turn on `RSpec.configuration.use_transactional_fixtures` in your `spec/rails_helper.rb`: ```ruby RSpec.configure do |config| config.use_transactional_fixtures = true # RSpec takes care to use `use_transactional_tests` or `use_transactional_fixtures` depending on the Rails version used end ``` Make sure to set `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1) to `true` if you're using Minitest. If you're using DatabaseCleaner, make sure it rolls back the database between tests. ## Aliases Naming is hard. Handling edge cases (the ones described above) is also tricky. To solve this we provide a way to define `let_it_be` aliases with the predefined options: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # define an alias with `refind: true` by default config.alias_to :let_it_be_with_refind, refind: true end # then use it in your tests describe "smth" do let_it_be_with_refind(:foo) { Foo.create } # refind can still be overridden let_it_be_with_refind(:bar, refind: false) { Bar.create } end ``` ## Modifiers If you modify objects generated within a `let_it_be` block in your examples, you maybe have to re-initiate them to avoid state leakage between the examples. Keep in mind that even though the database is rolled back to its pristine state, models themselves are not. We have a built-in *modifiers* support for getting models to their pristine state: ```ruby # Use reload: true option to reload user object (assuming it's an instance of ActiveRecord) # for every example let_it_be(:user, reload: true) { create(:user) } # it is almost equal to before_all { @user = create(:user) } let(:user) { @user.reload } # You can also specify refind: true option to hard-reload the record let_it_be(:user, refind: true) { create(:user) } # it is almost equal to before_all { @user = create(:user) } let(:user) { User.find(@user.id) } ``` **NOTE:** make sure that you require `let_it_be` after `active_record` is loaded (e.g., in `rails_helper.rb` **after** requiring the Rails app); otherwise the `refind` and `reload` modifiers are not activated. You can also use modifiers with array values, e.g. `create_list`: ```ruby let_it_be(:posts, reload: true) { create_list(:post, 3) } # it's the same as before_all { @posts = create_list(:post, 3) } let(:posts) { @posts.map(&:reload) } ``` ### Custom Modifiers If `reload` and `refind` is not enough, you can add your custom modifier: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # Define a block which will be called when you access a record first within an example. # The first argument is the pre-initialized record, # the second is the value of the modifier. # # This is how `reload` modifier is defined config.register_modifier :reload do |record, val| # ignore when `reload: false` next record unless val # ignore non-ActiveRecord objects next record unless record.is_a?(::ActiveRecord::Base) record.reload end end ``` ### Default Modifiers It's possible to configure the default modifiers used for all `let_it_be` calls: * Globally: ```ruby TestProf::LetItBe.configure do |config| # Make refind activated by default config.default_modifiers[:refind] = true end ``` * For specific contexts using tags: ```ruby context "with let_it_be reload", let_it_be_modifiers: {reload: true} do # examples end ``` **NOTE:** Nested contexts tags are overwritten not merged: ```ruby TestProf::LetItBe.configure do |config| config.default_modifiers[:freeze] = false end context "with reload", let_it_be_modifiers: {reload: true} do # uses freeze: false, reload: true here context "with freeze", let_it_be_modifiers: {freeze: true} do # uses only freeze: true (reload: true is overwritten by new metadata) end end ``` ## State Leakage Detection From [`rspec-rails` docs](https://relishapp.com/rspec/rspec-rails/v/3-9/docs/transactions) on transactions and `before(:context)`: > Even though database updates in each example will be rolled back, the object won't know about those rollbacks so the object and its backing data can easily get out of sync. Since `let_it_be` initialize objects in `before(:context)` hooks under the hood, it's affected by this problem: the code might modify models shared between examples (thus causing *shared state leaks*). That could happen unwillingly: when the underlying code under test modifies models, e.g. modifies `updated_at` attribute; or deliberately: when models are updated in `before` hooks or examples themselves instead of creating models in a proper state initially. This state leakage comes with potentially harmful side effects on the other examples, such as implicit dependencies and execution order dependency. With many shared models between many examples, it's hard to track down the example and exact place in the code that modifies the model. To detect modifications, objects that are passed to `let_it_be` are frozen (with `#freeze`), and `FrozenError` is raised: ```ruby # use freeze: true modifier to enable this feature let_it_be(:user, freeze: true) { create(:user) } # it is almost equal to before_all { @user = create(:user).freeze } let(:user) { @user } ``` To fix the `FrozenError`: * Add `reload: true`/`refind: true`, it pacifies leakage detection and prevents leakage itself. Typically it's significantly faster to reload the model than to re-create it from scratch before each example (two or even three orders of magnitude faster in some cases). * Rewrite the problematic test code. This feature is opt-in, since it may find a significant number of leakages in specs that may be a significant burden to fix all at once. It's possible to gradually turn it on for parts of specs (e.g., only models) by using: ```ruby # spec/spec_helper.rb RSpec.configure do |config| # ... config.define_derived_metadata(let_it_be_frost: true) do |metadata| metadata[:let_it_be_modifiers] ||= {freeze: true} end end ``` And then tag contexts/examples with `:let_it_be_frost` to enable this feature. Alternatively, you can specify `freeze` modifier explicitly (`let_it_be(freeze: true)`) or configure an alias. ## Report duplicates Although we suggest using `let_it_be` instead of `let!`, there is one important difference: you can override `let!` definition with the same or nested context, so only the latter one is called; `let_it_be` records could be overridden, but still created. For example: ```ruby context "A" do let!(:user) { create(:user, name: "a") } let_it_be(:post) { create(:post, title: "A") } specify { expect(User.all.pluck(:name)).to eq ["a"] } specify { expect(Post.all.pluck(:title)).to eq ["A"] } context "B" do let!(:user) { create(:user, name: "b") } let_it_be(:post) { create(:post, title: "B") } specify { expect(User.all.pluck(:name)).to eq ["b"] } specify { expect(Post.all.pluck(:title)).to eq ["B"] } # fails, because there are two posts end end ``` So for your convenience, you can configure the behavior when let\_it\_be is overridden. ```ruby TestProf::LetItBe.configure do |config| config.report_duplicates = :warn # Rspec.warn_with config.report_duplicates = :raise # Kernel.raise end ``` By default this parameter is disabled. You can configure the behavior that will generate a warning or raise an exception. --- --- url: /ja/guide/recipes/let_it_be.md --- # Let It Be Let's bring a little bit of magic and introduce a new way to set up a *shared* test data. Suppose you have the following setup: ```ruby describe BeatleWeightedSearchQuery do let!(:paul) { create(:beatle, name: "Paul") } let!(:ringo) { create(:beatle, name: "Ringo") } let!(:george) { create(:beatle, name: "George") } let!(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # and more examples here end ``` We don't need to re-create the Fab Four for every example, do we? We already have [`before_all`](./before_all.md) to solve the problem of *repeatable* data: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end specify { expect(subject.call("joh")).to contain_exactly(@john) } # ... end ``` That technique works pretty good but requires us to use instance variables and define everything at once. Thus it's not easy to refactor existing tests which use `let/let!` instead. With `let_it_be` you can do the following: ```ruby describe BeatleWeightedSearchQuery do let_it_be(:paul) { create(:beatle, name: "Paul") } let_it_be(:ringo) { create(:beatle, name: "Ringo") } let_it_be(:george) { create(:beatle, name: "George") } let_it_be(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # and more examples here end ``` That's it! Just replace `let!` with `let_it_be`. That's equal to the `before_all` approach but requires less refactoring. **NOTE**: Great superpower that `before_all` provides comes with a great responsibility. Make sure to check the [Caveats section](#caveats) of this document for details. ## Instructions In your `rails_helper.rb` or `spec_helper.rb`: ```ruby require "test_prof/recipes/rspec/let_it_be" ``` In your tests: ```ruby describe MySuperDryService do let_it_be(:user) { create(:user) } # ... end ``` `let_it_be` won't automatically bring the database to its previous state between the examples, it only does that between example groups. Use Rails' native `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1), RSpec Rails' `use_transactional_fixtures`, DatabaseCleaner, or custom code that begins a transaction before each test and rolls it back after. ## Caveats ### Database is rolled back to a pristine state, but the objects are not If you modify objects generated within a `let_it_be` block in your examples, you maybe have to re-initiate them. We have a built-in *modifiers* support for that. ### Database is not rolled back between tests Database is not rolled back between RSpec examples, only between example groups. We don't want to reinvent the wheel and encourage you to use other tools that provide this out of the box. If you're using RSpec Rails, turn on `RSpec.configuration.use_transactional_fixtures` in your `spec/rails_helper.rb`: ```ruby RSpec.configure do |config| config.use_transactional_fixtures = true # RSpec takes care to use `use_transactional_tests` or `use_transactional_fixtures` depending on the Rails version used end ``` Make sure to set `use_transactional_tests` (`use_transactional_fixtures` in Rails < 5.1) to `true` if you're using Minitest. If you're using DatabaseCleaner, make sure it rolls back the database between tests. ## Aliases Naming is hard. Handling edge cases (the ones described above) is also tricky. To solve this we provide a way to define `let_it_be` aliases with the predefined options: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # define an alias with `refind: true` by default config.alias_to :let_it_be_with_refind, refind: true end # then use it in your tests describe "smth" do let_it_be_with_refind(:foo) { Foo.create } # refind can still be overridden let_it_be_with_refind(:bar, refind: false) { Bar.create } end ``` ## Modifiers If you modify objects generated within a `let_it_be` block in your examples, you maybe have to re-initiate them to avoid state leakage between the examples. Keep in mind that even though the database is rolled back to its pristine state, models themselves are not. We have a built-in *modifiers* support for getting models to their pristine state: ```ruby # Use reload: true option to reload user object (assuming it's an instance of ActiveRecord) # for every example let_it_be(:user, reload: true) { create(:user) } # it is almost equal to before_all { @user = create(:user) } let(:user) { @user.reload } # You can also specify refind: true option to hard-reload the record let_it_be(:user, refind: true) { create(:user) } # it is almost equal to before_all { @user = create(:user) } let(:user) { User.find(@user.id) } ``` **NOTE:** make sure that you require `let_it_be` after `active_record` is loaded (e.g., in `rails_helper.rb` **after** requiring the Rails app); otherwise the `refind` and `reload` modifiers are not activated. You can also use modifiers with array values, e.g. `create_list`: ```ruby let_it_be(:posts, reload: true) { create_list(:post, 3) } # it's the same as before_all { @posts = create_list(:post, 3) } let(:posts) { @posts.map(&:reload) } ``` ### Custom Modifiers If `reload` and `refind` is not enough, you can add your custom modifier: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # Define a block which will be called when you access a record first within an example. # The first argument is the pre-initialized record, # the second is the value of the modifier. # # This is how `reload` modifier is defined config.register_modifier :reload do |record, val| # ignore when `reload: false` next record unless val # ignore non-ActiveRecord objects next record unless record.is_a?(::ActiveRecord::Base) record.reload end end ``` ### Default Modifiers It's possible to configure the default modifiers used for all `let_it_be` calls: * Globally: ```ruby TestProf::LetItBe.configure do |config| # Make refind activated by default config.default_modifiers[:refind] = true end ``` * For specific contexts using tags: ```ruby context "with let_it_be reload", let_it_be_modifiers: {reload: true} do # examples end ``` **NOTE:** Nested contexts tags are overwritten not merged: ```ruby TestProf::LetItBe.configure do |config| config.default_modifiers[:freeze] = false end context "with reload", let_it_be_modifiers: {reload: true} do # uses freeze: false, reload: true here context "with freeze", let_it_be_modifiers: {freeze: true} do # uses only freeze: true (reload: true is overwritten by new metadata) end end ``` ## State Leakage Detection From [`rspec-rails` docs](https://relishapp.com/rspec/rspec-rails/v/3-9/docs/transactions) on transactions and `before(:context)`: > Even though database updates in each example will be rolled back, the object won't know about those rollbacks so the object and its backing data can easily get out of sync. Since `let_it_be` initialize objects in `before(:context)` hooks under the hood, it's affected by this problem: the code might modify models shared between examples (thus causing *shared state leaks*). That could happen unwillingly: when the underlying code under test modifies models, e.g. modifies `updated_at` attribute; or deliberately: when models are updated in `before` hooks or examples themselves instead of creating models in a proper state initially. This state leakage comes with potentially harmful side effects on the other examples, such as implicit dependencies and execution order dependency. With many shared models between many examples, it's hard to track down the example and exact place in the code that modifies the model. To detect modifications, objects that are passed to `let_it_be` are frozen (with `#freeze`), and `FrozenError` is raised: ```ruby # use freeze: true modifier to enable this feature let_it_be(:user, freeze: true) { create(:user) } # it is almost equal to before_all { @user = create(:user).freeze } let(:user) { @user } ``` To fix the `FrozenError`: * Add `reload: true`/`refind: true`, it pacifies leakage detection and prevents leakage itself. Typically it's significantly faster to reload the model than to re-create it from scratch before each example (two or even three orders of magnitude faster in some cases). * Rewrite the problematic test code. This feature is opt-in, since it may find a significant number of leakages in specs that may be a significant burden to fix all at once. It's possible to gradually turn it on for parts of specs (e.g., only models) by using: ```ruby # spec/spec_helper.rb RSpec.configure do |config| # ... config.define_derived_metadata(let_it_be_frost: true) do |metadata| metadata[:let_it_be_modifiers] ||= {freeze: true} end end ``` And then tag contexts/examples with `:let_it_be_frost` to enable this feature. Alternatively, you can specify `freeze` modifier explicitly (`let_it_be(freeze: true)`) or configure an alias. --- --- url: /ru/guide/recipes/let_it_be.md --- # Let It Be Предлагаем перейти на новый уровень и добавить ещё щепотку магии для эффективной генерации тестовых данных! Рассмотрим пример создания данных для теста: ```ruby describe BeatleWeightedSearchQuery do let!(:paul) { create(:beatle, name: "Paul") } let!(:ringo) { create(:beatle, name: "Ringo") } let!(:george) { create(:beatle, name: "George") } let!(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # далее идут другие тесты end ``` Что бросается в глаза? Мы создаём *великолепную четвёрку* заново для каждого теста. И хотя Битлз много не бывает, на скорости выполнения наших тестов это сказывается негативно. Мы могли бы воспользоваться [`before_all`](./before_all.md) для решения проблема *повторяемых данных*: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end specify { expect(subject.call("joh")).to contain_exactly(@john) } # ... end ``` Данный подход работает, но имеет ряд недостатков с точки зрения ускорения тестов *малой кровью*: нам нужно заменить `let/let!` выражения, а также методы на переменные инстанса класса. С помощью `let_it_be` же мы можем сделать так: ```ruby describe BeatleWeightedSearchQuery do let_it_be(:paul) { create(:beatle, name: "Paul") } let_it_be(:ringo) { create(:beatle, name: "Ringo") } let_it_be(:george) { create(:beatle, name: "George") } let_it_be(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # далее идут другие тесты end ``` И всё! Меняем `let!` на `let_it_be`. «Под капотом» используется тот же `before_all`, но *внешний* API ближе к стандартному API RSpec. **Примечание**: Как говорил дядя Питера Паркера: «Чем больше сила, тем больше ответственность». Обязательно проверьте [раздел «Предостережения»](#предостережения) для более подробной информации. ## Инструкция В вашем `rails_helper.rb` или `spec_helper.rb` добавьте строчку: ```ruby require "test_prof/recipes/rspec/let_it_be" ``` Теперь вы можете использовать `let_it_be` в тестах: ```ruby describe MySuperDryService do let_it_be(:user) { create(:user) } # ... end ``` **Примечание**: `let_it_be` не оборачивает индивидуальные тесты в собственные транзакции базы данных. Используйте нативный функционал Rails, `use_transactional_tests`,(`use_transactional_fixtures` в Rails < 5.1), либо `use_transactional_fixtures` из `rspec-rails`, либо DatabaseCleaner, либо свой код, который будет создавать транзакцию перед тестом и откатывать ее после. ## Предостережения ### База данных откатывается в первоначальное состояние, но объекты - нет Если вы измените объекты, созданные в блоке `let_it_be`, в вашем тесте, то вам, возможно, придется повторно инициировать их. Для это вы можете использовать [модификаторы](#модификаторы). См. также [предостережения для `before_all`](./before_all.md#предостережения). ## Модификаторы Модификаторы позволяют совершать дополнительные действия над объектами, генерируемыми через `let_it_be`, перед **каждым тестом**. Это помогает решать проблему утечки состояния объектов между тестами (база данных *отказывается* после каждого примера, но Ruby объекты — нет). Доступны следующие модификаторы: ```ruby # используйте reload: true для перезагрузки объекта для каждого теста (предполагается, что вы используете ActiveRecord) let_it_be(:user, reload: true) { create(:user) } # эквивалентно before_all { @user = create(:user) } let(:user) { @user.reload } # модификатор refind: true инициирует полностью новый объект (в некоторых случаях reload недостаточно) let_it_be(:user, refind: true) { create(:user) } # эквивалентно before_all { @user = create(:user) } let(:user) { User.find(@user.id) } ``` **Примечение**: убедитесь, что `let_it_be` загружается после `active_record` (например, в `rails_helper.rb` **после** загрузки Rails приложения); в противном случае модификаторы `refind` и `reload` не будут доступны. Модификаторы могут также использоваться с массивами объектов, например, созданными с помощью `create_list`: ```ruby let_it_be(:posts, reload: true) { create_list(:post, 3) } # эквивалентно before_all { @posts = create_list(:post, 3) } let(:posts) { @posts.map(&:reload) } ``` ### Пользовательские модификаторы Вы можете добавить свои модификаторы: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # Так выглядит определение модификатора `reload`: # первый аргумент — это объект, второй — значение модификатора config.register_modifier :reload do |record, val| # игнорируем, если указано `reload: false` next record unless val # игнорируем не-ActiveRecord объекты next record unless record.is_a?(::ActiveRecord::Base) record.reload end end ``` ### Модификаторы по умолчанию Вы можете указать, какие модификаторы должны быть использованы для `let_it_be` по умолчанию: * глобально: ```ruby TestProf::LetItBe.configure do |config| config.default_modifiers[:refind] = true end ``` * для отдельных групп тестов, используя теги: ```ruby context "with let_it_be reload", let_it_be_modifiers: {reload: true} do # ... end ``` **Примечени**: Теги для вложенных контекстов перезаписываются: ```ruby TestProf::LetItBe.configure do |config| config.default_modifiers[:freeze] = false end context "with reload", let_it_be_modifiers: {reload: true} do # здесь будет использовано freeze: false, reload: true context "with freeze", let_it_be_modifiers: {freeze: true} do # здесь будет использовано только freeze: true (reload: true будет перезаписан) end end ``` ## Псевдонимы (*алиасы*) Не всем нравится Битлз 😞 Нет проблем — вы можете настроить псевдоним для метода `let_it_be` и использовать любое другое имя: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| config.alias_to :да_будет_так end ``` Вы также можете указывать модификаторы по умолчанию для псевдонимов: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| config.alias_to :let_it_be_with_refind, refind: true end describe "smth" do let_it_be_with_refind(:foo) { Foo.create } # refind может быть переопределён let_it_be_with_refind(:bar, refind: false) { Bar.create } end ``` ## Обнаружение утечек состояния Из [документации `rspec-rails`](https://relishapp.com/rspec/rspec-rails/v/3-9/docs/transactions) про `before(:context)`: > Even though database updates in each example will be rolled back, the object won't know about those rollbacks so the object and its backing data can easily get out of sync. Так как `let_it_be` выполняет код внутри `before(:context)`, мы имеем дело с описанной выше проблемой: Руби-объекты, переиспользуемые в тестах, могут изменяться, тем самым приводя к *утечкам*. Например, тестируемый код может изменять модели, обновлять значение `updated_at`; или в самих тестах вы можете изменять объекты в `before` хуках. Подобные утечки приводят к возникновению зависимостей между тестами и вносят нестабильность в успешность их выполнения. С другой стороны, обнаружить подобные проблемы бывает не так-то просто. Мы предлагаем специальный модификатор, `freeze`, который делает объекты, генерируемые с помощью `let_it_be`, неизменяемыми: ```ruby let_it_be(:user, freeze: true) { create(:user) } # эквивалентно before_all { @user = create(:user).freeze } let(:user) { @user } ``` Если после добавления модификатора `freeze` ваши тесты падают с ошибкой `FrozenError`, то: * добавьте `reload: true`/`refind: true`; в большинстве случаев это устраняет проблему. Перезагрузка модели (`reload`), как правило, значительное быстрее, чем пересоздание (`refind`), поэтому для начала рекоммендуем пробовать использовать `reload: true`; * перепишите проблемный тест. Для проектов, которые только начинают использовать `let_it_be`, мы рекомендуем активировать модификатор `freeze` по умолчанию (см. выше). Существующие проекты могут использовать теги для постепенного исправления тестов: ```ruby # spec/spec_helper.rb RSpec.configure do |config| # ... config.define_derived_metadata(let_it_be_frost: true) do |metadata| metadata[:let_it_be_modifiers] ||= {freeze: true} end end ``` --- --- url: /zh-cn/guide/recipes/let_it_be.md --- # Let It Be 让我们带来一点魔法,介绍一种建立\_shared\_测试数据的新方法。 假设你有如下设定: ```ruby describe BeatleWeightedSearchQuery do let!(:paul) { create(:beatle, name: "Paul") } let!(:ringo) { create(:beatle, name: "Ringo") } let!(:george) { create(:beatle, name: "George") } let!(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # and more examples here end ``` 我们不需要为每个测试用例都创建这个“四人组”,对吧? 已经有了 [`before_all`](./before_all.md) 来解决关于\_重复性数据\_的问题: ```ruby describe BeatleWeightedSearchQuery do before_all do @paul = create(:beatle, name: "Paul") # ... end specify { expect(subject.call("joh")).to contain_exactly(@john) } # ... end ``` 这个方法工作良好,但需要我们使用实例变量以及一次性定义所有东西。这样便不易于重构使用 `let/let!`的已有测试。 使用 `let_it_be` 你就能做到如下: ```ruby describe BeatleWeightedSearchQuery do let_it_be(:paul) { create(:beatle, name: "Paul") } let_it_be(:ringo) { create(:beatle, name: "Ringo") } let_it_be(:george) { create(:beatle, name: "George") } let_it_be(:john) { create(:beatle, name: "John") } specify { expect(subject.call("john")).to contain_exactly(john) } # and more examples here end ``` 完事!只用把 `let!` 替换为 `let_it_be`即可。这等价于 `before_all` 方案而只用更少的重构。 **注意**: `before_all` 提供的强大能力需要担负强大的责任。 确认参考 [Caveats section](#caveats) 文档的具体细节。 ## 教学 在 `rails_helper.rb` 或 `spec_helper.rb`中: ```ruby require "test_prof/recipes/rspec/let_it_be" ``` 在测试中: ```ruby describe MySuperDryService do let_it_be(:user) { create(:user) } # ... end ``` `let_it_be` 不会在测试用例之间自动把数据库恢复之前的状态,它仅在测试用例组之间这样做。 请使用 Rails 原生的 `use_transactional_tests` ( Rails < 5.1 中是`use_transactional_fixtures` ), RSpec Rails 的 `use_transactional_fixtures`,DatabaseCleaner,或者在每个测试用例前开始事务并在结束后回滚的自定义代码。 ## 警告 ### 数据库是被回滚到全新的初始状态,但对象并非如此。 如果你在测试用例中更改了 `let_it_be` 代码块中所生成的对象,那么可能不得不重新初始化它们。 我们有一个内置的\_修饰器\_支持这个。 ### 数据库在测试之间没有回滚 数据库在 RSpec 测试用例之间不回滚,仅在测试用例组之间回滚。 我们不想重新发明轮子,所以鼓励你使用其他自带支持该功能的工具。 如果你使用 RSpec Rails,在你的 `spec/rails_helper.rb`中打开`RSpec.configuration.use_transactional_fixtures`: ```ruby RSpec.configure do |config| config.use_transactional_fixtures = true # RSpec takes care to use `use_transactional_tests` or `use_transactional_fixtures` depending on the Rails version used end ``` 如果你使用 Minitest,请确认设置了 `use_transactional_tests`(在 Rails < 5.1 中是`use_transactional_fixtures` )为 `true` 。 如果你在使用 DatabaseCleaner,请确认其是在测试之间进行回滚。 ## Aliases > 自 v0.9.0 起 命名是很难的。处理边界场景(上述情况)也不容易。 为了解决这个问题,我们提供了一种方式以预定义选项来定义 `let_it_be` 的 aliases: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # define an alias with `refind: true` by default config.alias_to :let_it_be_with_refind, refind: true end # then use it in your tests describe "smth" do let_it_be_with_refind(:foo) { Foo.create } # refind can still be overridden let_it_be_with_refind(:bar, refind: false) { Bar.create } end ``` ## 修饰器 如果你修改了测试用例内的 `let_it_be` 代码块中所生成的对象,那么可能不得不重新初始化它们以避免在测试用例之间的状态泄漏。 要小心,尽管数据库能被回滚到其崭新状态,而 models 自身不会。 我们有一个内置的 *修饰器* 支持把 models 置为崭新的状态: ```ruby # Use reload: true option to reload user object (assuming it's an instance of ActiveRecord) # for every example let_it_be(:user, reload: true) { create(:user) } # it is almost equal to before_all { @user = create(:user) } let(:user) { @user.reload } # You can also specify refind: true option to hard-reload the record let_it_be(:user, refind: true) { create(:user) } # it is almost equal to before_all { @user = create(:user) } let(:user) { User.find(@user.id) } ``` **注意:** 请确保你 require `let_it_be` 是在 `active_record` 被加载之后(比如,在 `rails_helper.rb` 中是在 require Rails app **之后**);否则, `refind` 和 `reload` 修饰器不会被激活。 (**自 v0.10.0 起**)你也可以把修饰器跟数组值一起使用了,比如, `create_list`: ```ruby let_it_be(:posts, reload: true) { create_list(:post, 3) } # it's the same as before_all { @posts = create_list(:post, 3) } let(:posts) { @posts.map(&:reload) } ``` ### 自定义修饰器 > 自 v0.10.0 起 如果 `reload` 和 `refind` 还不够,那么你可以添加自己的自定义修饰器: ```ruby # rails_helper.rb TestProf::LetItBe.configure do |config| # Define a block which will be called when you access a record first within an example. # The first argument is the pre-initialized record, # the second is the value of the modifier. # # This is how `reload` modifier is defined config.register_modifier :reload do |record, val| # ignore when `reload: false` next record unless val # ignore non-ActiveRecord objects next record unless record.is_a?(::ActiveRecord::Base) record.reload end end ``` ### 默认修饰器 > 自 v0.12.0 起 对于所有 `let_it_be` 调用,可以配置其默认修饰器: * 全局: ```ruby TestProf::LetItBe.configure do |config| # Make refind activated by default config.default_modifiers[:refind] = true end ``` * 对使用 tags 的特定 contexts: ```ruby context "with let_it_be reload", let_it_be_modifiers: {reload: true} do # examples end ``` **注意** 嵌套 contexts tags 是被覆盖而非合并: ```ruby TestProf::LetItBe.configure do |config| config.default_modifiers[:freeze] = false end context "with reload", let_it_be_modifiers: {reload: true} do # uses freeze: false, reload: true here context "with freeze", let_it_be_modifiers: {freeze: true} do # uses only freeze: true (reload: true is overwritten by new metadata) end end ``` ## 状态泄漏的检测 > 自 v0.12.0 起 [`rspec-rails` docs](https://relishapp.com/rspec/rspec-rails/v/3-9/docs/transactions) 对于事务和 `before(:context)`的描述: > 即使每个测试用例中的数据库更新都将回滚,对象也不会知道这些回滚,因此该对象与其数据很容易不同步。 由于 `let_it_be` 是在其引擎下的 `before(:context)` hooks 中初始化对象,因此会受到该问题影响:代码可能会修改测试用例之间共享的 models(因此造成\_共享状态的泄漏\_)。这可以是非主动地:当测试之下的基础代码修改了 models 时,比如,修改了 `updated_at` 属性;或者可以是故意地:当 models 在 `before` hooks 或测试用例自身中被更新,而非在初始的适当状态中被创建的时候。 这种状态泄漏带来了对其他测试用例的潜在有害的边际效应,比如隐式的依赖和执行顺序依赖。 对多个测试用例之间的多个共享 models,很难追踪这些用例和修改 model 的代码的准确位置。 要检测更改,被传给 `let_it_be` 的对象被冻结(以 `#freeze`),并且 `FrozenError` 被抛出: ```ruby # use freeze: true modifier to enable this feature let_it_be(:user, freeze: true) { create(:user) } # it is almost equal to before_all { @user = create(:user).freeze } let(:user) { @user } ``` 要修复 `FrozenError`: * 添加 `reload: true`/`refind: true`, 它可消除泄漏检测并防止泄漏本身。通常在每个测试用例之前重新载入 model 比从头重新创建 model 要明显更快(在某些情况下要快两个甚至三个数量级)。 * 重写造成问题的测试代码。 该功能是可选的,因为它可能会发现 specs 中的大量泄漏,而一次性修复所有泄漏可能是一种重大负担。 可以针对部分 specs 逐步打开它(比如,仅针对 models),如下: ```ruby # spec/spec_helper.rb RSpec.configure do |config| # ... config.define_derived_metadata(let_it_be_frost: true) do |metadata| metadata[:let_it_be_modifiers] ||= {freeze: true} end end ``` And then tag然后给 contexts 或测试用例打上 `:let_it_be_frost` 的 tag 来启用该功能。 或者,你还可以明确指定 `freeze` 修饰器(`let_it_be(freeze: true)`)或者配置一个 alias。 --- --- url: /guide/profilers/memory_prof.md --- # MemoryProf MemoryProf tracks memory usage during your test suite run, and can help to detect test examples and groups that cause memory spikes. Memory profiling supports the following metrics: RSS, allocations and GC time. Example output: ```sh [TEST PROF INFO] MemoryProf results Final RSS: 673KB Top 5 groups (by RSS): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – +80KB (13.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – +32KB (9.08%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – +16KB (3.27%) Top 5 examples (by RSS): destroys question (./spec/controllers/questions_controller_spec.rb:38) – +144KB (24.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – +120KB (20.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – +90KB (16.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – +64KB (12.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – +32KB (5.00%) ``` The output shows the amount of memory used by each example and top-level example group (typically, a test file). ## Instructions To activate MemoryProf with: ### RSpec Use `TEST_MEM_PROF` environment variable to set which metric to use: ```sh TEST_MEM_PROF='rss' rspec ... TEST_MEM_PROF='alloc' rake rspec ... TEST_MEM_PROF='gc' rake rspec ... ``` ### Minitest In Minitest 6+, you must first activate TestProf plugin by adding `Minitest.load :test_prof` in your test helper. Use `TEST_MEM_PROF` environment variable to set which metric to use: ```sh TEST_MEM_PROF='rss' rake test TEST_MEM_PROF='alloc' rake test TEST_MEM_PROF='gc' rake test ``` or use CLI options as well: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --mem-prof=rss # Show the list of possible options: ruby test/my_super_test.rb --help ``` ## Configuration By default, MemoryProf tracks the top 5 examples and groups that use the largest amount of memory. You can set how many examples/groups to display with the option: ```sh TEST_MEM_PROF='rss' TEST_MEM_PROF_COUNT=10 rspec ... ``` or with CLI options for Minitest: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --mem-prof=rs --mem-prof-top-count=10 ``` ## Supported Ruby Engines & OS Currently the allocation mode is not supported for JRuby. Since RSS depends on the OS, MemoryProf uses different tools to retrieve it: * Linux – `/proc/$pid/statm` file, * macOS, Solaris, BSD – `ps`, * Windows – `Get-Process`, requires PowerShell to be installed. --- --- url: /ja/guide/profilers/memory_prof.md --- # MemoryProf MemoryProf tracks memory usage during your test suite run, and can help to detect test examples and groups that cause memory spikes. Memory profiling supports two metrics: RSS and allocations. Example output: ```sh [TEST PROF INFO] MemoryProf results Final RSS: 673KB Top 5 groups (by RSS): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – +80KB (13.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – +32KB (9.08%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – +16KB (3.27%) Top 5 examples (by RSS): destroys question (./spec/controllers/questions_controller_spec.rb:38) – +144KB (24.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – +120KB (20.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – +90KB (16.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – +64KB (12.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – +32KB (5.00%) ``` The examples block shows the amount of memory used by each example, and the groups block displays the memory allocated by other code defined in the groups. For example, RSpec groups may include heavy `before(:all)` (or `before_all`) setup blocks, so it is helpful to see which groups use the most amount of memory outside of their examples. ## Instructions To activate MemoryProf with: ### RSpec Use `TEST_MEM_PROF` environment variable to set which metric to use: ```sh TEST_MEM_PROF='rss' rspec ... TEST_MEM_PROF='alloc' rake rspec ... ``` ### Minitest Use `TEST_MEM_PROF` environment variable to set which metric to use: ```sh TEST_MEM_PROF='rss' rake test TEST_MEM_PROF='alloc' rspec ... ``` or use CLI options as well: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --mem-prof=rss # Show the list of possible options: ruby test/my_super_test.rb --help ``` ## Configuration By default, MemoryProf tracks the top 5 examples and groups that use the largest amount of memory. You can set how many examples/groups to display with the option: ```sh TEST_MEM_PROF='rss' TEST_MEM_PROF_COUNT=10 rspec ... ``` or with CLI options for Minitest: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --mem-prof=rs --mem-prof-top-count=10 ``` ## Supported Ruby Engines & OS Currently the allocation mode is not supported for JRuby. Since RSS depends on the OS, MemoryProf uses different tools to retrieve it: * Linux – `/proc/$pid/statm` file, * macOS, Solaris, BSD – `ps`, * Windows – `Get-Process`, requires PowerShell to be installed. --- --- url: /guide/playbook.md --- # Playbook This document aims to help you get started with profiling test suites and answers the following questions: which profiles to run first? How do we interpret the results to choose the next steps? Etc. **NOTE**: This document assumes you're working with a Ruby on Rails application and RSpec testing framework. The ideas can easily be translated into other frameworks. > 📼 Check out also the ["From slow to go" RailsConf 2024 workshop recording](https://evilmartians.com/events/from-slow-to-go-rails-test-profiling-hands-on-railsconf-2024) to see this playbook in action. ## Step 0. Configuration basics Low-hanging configuration fruits: * Disable logging\* in tests—it's useless. If you really need it, use our [logging utils](./recipes/logging.md). ```ruby config.logger = ActiveSupport::TaggedLogging.new(Logger.new(nil)) config.log_level = :fatal ``` * Disable coverage and built-in profiling by default. Use env var to enable it (e.g., `COVERAGE=true`) \* Modern SSD hard drives make the overhead of file-based logging almost negligible. Still, we recommend disabling logging to make sure tests are not affected in any environment (e.g., Docker on MacOS). ## Step 1. General profiling It helps to identify not-so-low hanging fruits. We recommend using [StackProf](./profilers/ruby_profilers.md#stack_prof) or [Vernier](./profilers/ruby_profilers.md#vernier), so you must install them first (if not yet): ```sh bundle add stackprof # or bundle add vernier ``` Configure TestProf to generate JSON profiles by default: ```ruby TestProf::StackProf.configure do |config| config.format = "json" end ``` We recommend using [speedscope](https://www.speedscope.app) to analyze these profiles. ### Step 1.1. Application boot profiling ```sh TEST_STACK_PROF=boot rspec ./spec/some_spec.rb ``` **NOTE:** running a single spec/test is enough for this profiling. What to look for? Some examples: * No [Bootsnap](https://github.com/Shopify/bootsnap) used or not configured to cache everything (e.g., YAML files) * Slow Rails initializers that are not needed in tests. Vernier's Rails hooks feature is especially useful in analyzing Rails initializers. ### Step 1.2. Sampling tests profiling The idea is to run a random subset of tests multiple times to reveal some application-wide problems. You must enable the [sampling feature](./recipes/tests_sampling.md) first: ```rb # For RSpec in your spec_helper.rb require "test_prof/recipes/rspec/sample" # For Minitest in your test_helper.rb require "test_prof/recipes/minitest/sample" ``` Then run **multiple times** and analyze the obtained flamegraphs: ```sh SAMPLE=100 bin/rails test # or SAMPLE=100 bin/rspec ``` Common findings: * Encryption calls (`*crypt*`-whatever): relax the settings in the test env * Log calls: are you sure you disabled logs? * Databases: maybe there are some low-hanging fruits (like using DatabaseCleaner truncation for every test instead of transactions) * Network: should not be there for unit tests, inevitable for browser tests; use [Webmock](https://github.com/bblimke/webmock) to disable HTTP calls completely. ## Step 2. Narrow down the scope This is an important step for large codebases. We must prioritize quick fixes that bring the most value (time reduction) over dealing with complex, slow tests individually (even if they're the slowest ones). For that, we first identify the **types of tests** contributing the most to the overall run time. We use [TagProf](./profilers/tag_prof.md) for that: ```sh TAG_PROF=type TAG_PROF_FORMAT=html TAG_PROF_EVENT=sql.active_record,factory.create bin/rspec ``` Looking at the generated diagram, you can identify the two most time-consuming test types (usually models and/or controllers among them). We assume that it's easier to find a common slowness cause for the whole group and fix it than dealing with individual tests. Given that assumption, we continue the process only within the selected group (let's say, models). ## Step 3. Specialized profiling Within the selected group, we can first perform quick event-based profiling via [EventProf](./profilers/event_prof.md). (Maybe, with sampling enabled as well). ### Step 3.1. Dependencies configuration At this point, we may identify some misconfigured or misused dependencies/gems. Common examples: * Inlined Sidekiq jobs: ```sh EVENT_PROF=sidekiq.inline bin/rspec spec/models ``` * Wisper broadcasts ([patch required](https://gist.github.com/palkan/aa7035cebaeca7ed76e433981f90c07b)): ```sh EVENT_PROF=wisper.publisher.broadcast bin/rspec spec/models ``` * PaperTrail logs creation: Enable custom profiling: ```rb TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_create) TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_destroy) TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_update) ``` Run tests: ```sh EVENT_PROF=paper_trail.record bin/rspec spec/models ``` See [the Sidekiq example](https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests#background-jobs) on how to quickly fix such problems using [RSpecStamp](./recipes/rspec_stamp.md). ### Step 3.2. Data generation Identify the slowest tests based on the amount of time spent in the database or factories (if any): ```sh # Database interactions EVENT_PROF=sql.active_record bin/rspec spec/models # Factories EVENT_PROF=factory.create bin/rspec spec/models ``` Now, we can narrow our scope further to the top 10 files from the generated reports. If you use factories, use the `factory.create` report. **TIP:** In RSpec, you can mark the slowest examples with a custom tag automatically using the following command: ```sh EVENT_PROF=factory.create EVENT_PROF_STAMP=slow:factory bin/rspec spec/models ``` ## Step 4. Factories usage Identify the most used factories among the `slow:factory` tests: ```sh FPROF=1 bin/rspec --tag slow:factory ``` If you see some factories used much more times than the total number of examples, you deal with *factory cascades*. Visualize the cascades: ```sh FPROF=flamegraph bin/rspec --tag slow:factory ``` The visualization should help to identify the factories to be fixed. You find possible solutions in [this post](https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest). ### Step 4.1. Factory defaults One option to fix cascades produced by model associations is to use [factory defaults](./recipes/factory_default.md). To estimate the potential impact and identify factories to apply this pattern to, run the following profiler: ```sh FACTORY_DEFAULT_PROF=1 bin/rspec --tag slow:factory ``` Try adding `create_default` and measure the impact: ```sh FACTORY_DEFAULT_SUMMARY=1 bin/rspec --tag slow:factory # More hits — better FactoryDefault summary: hit=11 miss=3 ``` ### Step 4.2. Factory fixtures Back to the `FPROF=1` report, see if you have some records created for every example (typically, `user`, `account`, `team`). Consider replacing them with fixtures using [AnyFixture](./recipes/any_fixture.md). ## Step 5. Reusable setup It's common to have the same setup shared across multiple examples. You can measure the time spent in `let` / `before` compared to the actual example time using [RSpecDissect](./profilers/rspec_dissect.md): ```sh RD_PROF=1 bin/rspec ``` Take a look at the slowest groups and try to replace `let`/`let!` with [let\_it\_be](./recipes/let_it_be.md) and `before` with [before\_all](./recipes/before_all.md). **IMPORTANT:** Knapsack Pro users must be aware that per-example balancing eliminates the positive effect of using `let_it_be` / `before_all`. You must switch to per-file balancing while at the same time keeping your files small—that's how you can maximize the effect of using Test Prof optimizations. ## Conclusion After applying the steps above to a given group of tests, you should develop the patterns and techniques optimized for your codebase. Then, all you need is to extrapolate them to other groups. Good luck! --- --- url: /ja/guide/playbook.md --- # Playbook This document aims to help you get started with profiling test suites and answers the following questions: which profiles to run first? How do we interpret the results to choose the next steps? Etc. **NOTE**: This document assumes you're working with a Ruby on Rails application and RSpec testing framework. The ideas can easily be translated into other frameworks. ## Step 0. Configuration basics Low-hanging configuration fruits: * Disable logging in tests—it's useless. If you really need it, use our [logging utils](./recipes/logging.md). ```ruby config.logger = ActiveSupport::TaggedLogging.new(Logger.new(nil)) config.log_level = :fatal ``` * Disable coverage and built-in profiling by default. Use env var to enable it (e.g., `COVERAGE=true`) ## Step 1. General profiling It helps to identify not-so-low hanging fruits. We recommend using [StackProf](./profilers/stack_prof.md), so you must install it first (if not yet): ```sh bundle add stackprof ``` Configure Test Prof to generate JSON profiles by default: ```ruby TestProf::StackProf.configure do |config| config.format = "json" end ``` We recommend using [speedscope](https://www.speedscope.app) to analyze these profiles. ### Step 1.1. Application boot profiling ```sh TEST_STACK_PROF=boot rspec ./spec/some_spec.rb ``` **NOTE:** running a single spec/test is enough for this profiling. What to look for? Some examples: * No [Bootsnap](https://github.com/Shopify/bootsnap) used or not configured to cache everything (e.g., YAML files) * Slow Rails initializers that are not needed in tests. ### Step 1.2. Sampling tests profiling The idea is to run a random subset of tests multiple times to reveal some application-wide problems. You must enable the [sampling feature](./recipes/tests_sampling.md) first: ```rb # For RSpec in your spec_helper.rb require "test_prof/recipes/rspec/sample" # For Minitest in your test_helper.rb require "test_prof/recipes/minitest/sample" ``` Then run **multiple times** and analyze the obtained flamegraphs: ```sh SAMPLE=100 bin/rails test # or SAMPLE=100 bin/rspec ``` Common findings: * Encryption calls (`*crypt*`-whatever): relax the settings in the test env * Log calls: are you sure you disabled logs? * Databases: maybe there are some low-hanging fruits (like using DatabaseCleaner truncation for every test instead of transactions) * Network: should not be there for unit tests, inevitable for browser tests; use [Webmock](https://github.com/bblimke/webmock) to disable HTTP calls completely. ## Step 2. Narrow down the scope This is an important step for large codebases. We must prioritize quick fixes that bring the most value (time reduction) over dealing with complex, slow tests individually (even if they're the slowest ones). For that, we first identify the **types of tests** contributing the most to the overall run time. We use [TagProf](./profilers/tag_prof.md) for that: ```sh TAG_PROF=type TAG_PROF_FORMAT=html TAG_PROF_EVENT=sql.active_record,factory.create bin/rspec ``` Looking at the generated diagram, you can identify the two most time-consuming test types (usually models and/or controllers among them). We assume that it's easier to find a common slowness cause for the whole group and fix it than dealing with individual tests. Given that assumption, we continue the process only within the selected group (let's say, models). ## Step 3. Specialized profiling Within the selected group, we can first perform quick event-based profiling via [EventProf](./profilers/event_prof.md). (Maybe, with sampling enabled as well). ### Step 3.1. Dependencies configuration At this point, we may identify some misconfigured or misused dependencies/gems. Common examples: * Inlined Sidekiq jobs: ```sh EVENT_PROF=sidekiq.inline bin/rspec spec/models ``` * Wisper broadcasts ([patch required](https://gist.github.com/palkan/aa7035cebaeca7ed76e433981f90c07b)): ```sh EVENT_PROF=wisper.publisher.broadcast bin/rspec spec/models ``` * PaperTrail logs creation: Enable custom profiling: ```rb TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_create) TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_destroy) TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_update) ``` Run tests: ```sh EVENT_PROF=paper_trail.record bin/rspec spec/models ``` See [the Sidekiq example](https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests#background-jobs) on how to quickly fix such problems using [RSpecStamp](./recipes/rspec_stamp.md). ### Step 3.2. Data generation Identify the slowest tests based on the amount of time spent in the database or factories (if any): ```sh # Database interactions EVENT_PROF=sql.active_record bin/rspec spec/models # Factories EVENT_PROF=factory.create bin/rspec spec/models ``` Now, we can narrow our scope further to the top 10 files from the generated reports. If you use factories, use the `factory.create` report. **TIP:** In RSpec, you can mark the slowest examples with a custom tag automatically using the following command: ```sh EVENT_PROF=factory.create EVEN_PROF_STAMP=slow:factory bin/rspec spec/models ``` ## Step 4. Factories usage Identify the most used factories among the `slow:factory` tests: ```sh FPROF=1 bin/rspec --tag slow:factory ``` If you see some factories used much more times than the total number of examples, you deal with *factory cascades*. Visualize the cascades: ```sh FPROF=flamegraph bin/rspec --tag slow:factory ``` The visualization should help to identify the factories to be fixed. You find possible solutions in [this post](https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest). ### Step 4.1. Factory defaults One option to fix cascades produced by model associations is to use [factory defaults](./recipes/factory_default.md). To estimate the potential impact and identify factories to apply this pattern to, run the following profiler: ```sh FACTORY_DEFAULT_PROF=1 bin/rspec --tag slow:factory ``` Try adding `create_default` and measure the impact: ```sh FACTORY_DEFAULT_SUMMARY=1 bin/rspec --tag slow:factory # More hits — better FactoryDefault summary: hit=11 miss=3 ``` ### Step 4.2. Factory fixtures Back to the `FPROF=1` report, see if you have some records created for every example (typically, `user`, `account`, `team`). Consider replacing them with fixtures using [AnyFixture](./recipes/any_fixture.md). ## Step 5. Reusable setup It's common to have the same setup shared across multiple examples. You can measure the time spent in `let` / `before` compared to the actual example time using [RSpecDissect](./profilers/rspec_dissect.md): ```sh RD_PROF=1 bin/rspec ``` Take a look at the slowest groups and try to replace `let`/`let!` with [let\_it\_be](./recipes/let_it_be.md) and `before` with [before\_all](./recipes/before_all.md). **IMPORTANT:** Knapsack Pro users must be aware that per-example balancing eliminates the positive effect of using `let_it_be` / `before_all`. You must switch to per-file balancing while at the same time keeping your files small—that's how you can maximize the effect of using Test Prof optimizations. ## Conclusion After applying the steps above to a given group of tests, you should develop the patterns and techniques optimized for your codebase. Then, all you need is to extrapolate them to other groups. Good luck! --- --- url: /guide/profilers/rspec_dissect.md --- # RSpecDissect Do you know how much time you spend in `before` hooks? Or in memoization helpers such as `let`? Usually, the most of the whole test suite time. *RSpecDissect* provides this kind of information and also shows you the example groups with the highest **setup time**. The main purpose of RSpecDissect is to identify these slow groups and refactor them using [`before_all`](../recipes/before_all.md) or [`let_it_be`](../recipes/let_it_be.md) recipes. Example output: ```sh [TEST PROF INFO] RSpecDissect report Total time: 25:14.870 Total setup time: 14:36.482 Top 5 slowest suites setup time: Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:29.895 of 00:33.706 / 327 (before: 00:25.346, before let: 00:19.389, lazy let: 00:04.543) ↳ user – 00:09.323 (327) ↳ funnel – 00:06.231 (122) ↳ account – 00:04.360 (91) FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:22.117 of 00:43.649 / 133 (before: 00:22.117, before let: 00:18.695, lazy let: 00:00.000) ↳ user – 00:09.323 (327) ↳ funnel – 00:06.231 (122) ... ``` The **setup time** includes the time spent in `before(:each)` hooks and lazy `let` definitions (except `subject`—it's often used to define the action under test, and, thus, shouldn't be considered a part of the setup phase). As you can see, the profiler also provides an information about the most time consuming `let` definitions. ## Instructions RSpecDissect can only be used with RSpec (which is clear from the name). To activate RSpecDissect use `RD_PROF` environment variable: ```sh RD_PROF=1 rspec ... ``` You can also specify the number of top slow groups through `RD_PROF_TOP` variable: ```sh RD_PROF=1 RD_PROF_TOP=10 rspec ... ``` You can also specify the number of top `let` declarations to print through `RD_PROF_LET_TOP=10` env var. ## Using with RSpecStamp RSpecDissect can be used with [RSpec Stamp](../recipes/rspec_stamp.md) to automatically mark *slow* examples with custom tags. For example: ```sh RD_PROF=1 RD_PROF_STAMP="slow" rspec ... ``` After running the command above the slowest example groups would be marked with the `:slow` tag. --- --- url: /ja/guide/profilers/rspec_dissect.md --- # RSpecDissect Do you know how much time you spend in `before` hooks? Or in memoization helpers such as `let`? Usually, the most of the whole test suite time. *RSpecDissect* provides this kind of information and also shows you the worst example groups. The main purpose of RSpecDissect is to identify these slow groups and refactor them using [`before_all`](../recipes/before_all.md) or [`let_it_be`](../recipes/let_it_be.md) recipes. Example output: ```sh [TEST PROF INFO] RSpecDissect enabled Total time: 25:14.870 Total `before(:each)` time: 14:36.482 Total `let` time: 19:20.259 Top 5 slowest suites (by `before(:each)` time): Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:29.895 of 00:33.706 (327) FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:22.117 of 00:43.649 (133) ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:21.220 of 00:41.407 (222) BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:15.729 of 00:27.893 (50) Analytics::Wor...rsion::Summary (./spec/services/analytics/workflow_conversion/summary_spec.rb:3) – 00:15.383 of 00:15.914 (12) Top 5 slowest suites (by `let` time): FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:38.532 of 00:43.649 (133) ↳ user – 3 ↳ funnel – 2 ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:33.252 of 00:41.407 (222) ↳ user – 10 ↳ funnel – 5 ↳ applicant – 2 Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:30.320 of 00:33.706 (327) ↳ user – 30 BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:25.710 of 00:27.893 e(50) ↳ user – 21 ↳ stage – 14 AvailableSlotsController (./spec/controllers/available_slots_controller_spec.rb:3) – 00:18.481 of 00:23.366 (85) ↳ user – 15 ↳ stage – 10 ``` As you can see, the `let` profiler also tracks the provides the information on how many times each `let` declarations has been used within a group (shows top-3 by default). ## Instructions RSpecDissect can only be used with RSpec (which is clear from the name). To activate RSpecDissect use `RD_PROF` environment variable: ```sh RD_PROF=1 rspec ... ``` You can also specify the number of top slow groups through `RD_PROF_TOP` variable: ```sh RD_PROF=1 RD_PROF_TOP=10 rspec ... ``` You can also track only `let` or `before` usage by specifying `RD_PROF=let` and `RD_PROF=before` respectively. For `let` profiler you can also specify the number of top `let` declarations to print through `RD_PROF_LET_TOP=10` env var. To disable `let` stats add: ```ruby TestProf::RSpecDissect.configure do |config| config.let_stats_enabled = false end ``` ## Using with RSpecStamp RSpecDissect can be used with [RSpec Stamp](../recipes/rspec_stamp.md) to automatically mark *slow* examples with custom tags. For example: ```sh RD_PROF=1 RD_PROF_STAMP="slow" rspec ... ``` After running the command above the slowest example groups would be marked with the `:slow` tag. --- --- url: /ru/guide/profilers/rspec_dissect.md --- # RSpecDissect Вы когда-нибудь задавались вопросом, сколько времени ваших тестов уходит на подготовку контекста в `before` хука? Или для вычисления значений ленивых `let`? Обычно, именно *там* большую часть времени проводят ваши тесты. *RSpecDissect* — это специальный профилировщик для RSpec, который показывает вам эту информацию и выводит список групп тестов, наиболее *злоупотребляющих* использованием данных конструкций. Основная задача данного профилировщика — обнаружить тесты, которые можно ускорить, применив [`before_all`](../recipes/before_all.md) или [`let_it_be`](../recipes/let_it_be.md). Пример отчёта: ```sh [TEST PROF INFO] RSpecDissect enabled Total time: 25:14.870 Total `before(:each)` time: 14:36.482 Total `let` time: 19:20.259 Top 5 slowest suites (by `before(:each)` time): Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:29.895 of 00:33.706 (327) FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:22.117 of 00:43.649 (133) ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:21.220 of 00:41.407 (222) BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:15.729 of 00:27.893 (50) Analytics::Wor...rsion::Summary (./spec/services/analytics/workflow_conversion/summary_spec.rb:3) – 00:15.383 of 00:15.914 (12) Top 5 slowest suites (by `let` time): FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:38.532 of 00:43.649 (133) ↳ user – 3 ↳ funnel – 2 ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:33.252 of 00:41.407 (222) ↳ user – 10 ↳ funnel – 5 ↳ applicant – 2 Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:30.320 of 00:33.706 (327) ↳ user – 30 BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:25.710 of 00:27.893 e(50) ↳ user – 21 ↳ stage – 14 AvailableSlotsController (./spec/controllers/available_slots_controller_spec.rb:3) – 00:18.481 of 00:23.366 (85) ↳ user – 15 ↳ stage – 10 ``` Как можно заметить выше, статистика использования `let` также включает информацию о количестве вызовов каждого определения (по умолчанию показываются только top-3 самых популярных). ## Инструкции RSpecDissect работает только с RSpec (как можно было догадаться из названия). Для активации RSpecDissect используйте переменную окружения `RD_PROF`: ```sh RD_PROF=1 rspec ... ``` Вы также можете указать, какое количество групп тестов выводить в отчёте, с помощью переменной окружения `RD_PROF_TOP`: ```sh RD_PROF=1 RD_PROF_TOP=10 rspec ... ``` Если вы хотите анализировать только `let` или только `before`, используйте `RD_PROF=let` и `RD_PROF=before` соответственно. Вы можете изменить максимальное число выводимых в отчёте `let`-определений с помощью переменной `RD_PROF_LET_TOP` (например, `RD_PROF_LET_TOP=10`). Наконец, чтобы отключить сбор статистики по `let`-определениям, укажите в конфигурации: ```ruby TestProf::RSpecDissect.configure do |config| config.let_stats_enabled = false end ``` ## Интеграция с RSpecStamp RSpecDissect интегрируется с [RSpec Stamp](../recipes/rspec_stamp.md) для автоматической пометки *медленных* тестов тегами. Например: ```sh RD_PROF=1 RD_PROF_STAMP="slow:setup" rspec ... ``` После запуска тестов с данными параметрами самые *медленные* группы тестов будут помечены тегом `slow: :setup`. --- --- url: /zh-cn/guide/profilers/rspec_dissect.md --- # RSpecDissect 分析器 你知道在`before` hooks 上花费了多少时间吗?或者在诸如 `let` 之类的帮助方法上?通常,它们是整个测试套件中耗费最多的时间。 *RSpecDissect* 提供了这种信息,也会向你展示最坏的测试用例组。RSpecDissect 的主要目标是识别出那些慢点用例组,并使用 [`before_all`](../recipes/before_all.md) 或 [`let_it_be`](../recipes/let_it_be.md) 的配方来重构它们。 输出范例: ```sh [TEST PROF INFO] RSpecDissect enabled Total time: 25:14.870 Total `before(:each)` time: 14:36.482 Total `let` time: 19:20.259 Top 5 slowest suites (by `before(:each)` time): Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:29.895 of 00:33.706 (327) FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:22.117 of 00:43.649 (133) ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:21.220 of 00:41.407 (222) BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:15.729 of 00:27.893 (50) Analytics::Wor...rsion::Summary (./spec/services/analytics/workflow_conversion/summary_spec.rb:3) – 00:15.383 of 00:15.914 (12) Top 5 slowest suites (by `let` time): FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:38.532 of 00:43.649 (133) ↳ user – 3 ↳ funnel – 2 ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:33.252 of 00:41.407 (222) ↳ user – 10 ↳ funnel – 5 ↳ applicant – 2 Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:30.320 of 00:33.706 (327) ↳ user – 30 BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:25.710 of 00:27.893 e(50) ↳ user – 21 ↳ stage – 14 AvailableSlotsController (./spec/controllers/available_slots_controller_spec.rb:3) – 00:18.481 of 00:23.366 (85) ↳ user – 15 ↳ stage – 10 ``` 如你所见, `let` 分析器也追踪、提供了组内(默认是排在前三位的)每个`let`声明被使用了多少次的信息。 ## 教学 RSpecDissect 只能跟 RSpec 一起使用(顾名思义)。 使用 `RD_PROF` 环境变量来激活 RSpecDissect: ```sh RD_PROF=1 rspec ... ``` 你也可以通过 `RD_PROF_TOP` 变量指定展示前几个最慢的组: ```sh RD_PROF=1 RD_PROF_TOP=10 rspec ... ``` 你也可以通过各自指定 `RD_PROF=let` 和 `RD_PROF=before`来仅追踪`let`或`before`的使用。 对于 `let` 分析器,你也可以通过 `RD_PROF_LET_TOP=10` 环境变量指定打印排位靠前的`let`声明的数量。 要禁用 `let` 统计,添加这个设置: ```ruby TestProf::RSpecDissect.configure do |config| config.let_stats_enabled = false end ``` ## 与 RSpecStamp 一起使用 RSpecDissect 可以跟 [RSpec Stamp](../recipes/rspec_stamp.md) 一起使用,以自定义 tag 来自动标记 *慢* 测试用例。例如: ```sh RD_PROF=1 RD_PROF_STAMP="slow" rspec ... ``` 运行上面命令后,最慢的测试用例组就以`:slow` tag 被标记了。 --- --- url: /guide/recipes/rspec_stamp.md --- # RSpecStamp RSpecStamp is a tool to automatically *tag* failed examples with custom tags. It *literally* adds tags to your examples (i.e. rewrites them). The main purpose of RSpecStamp is to make testing codebase refactoring easy. Changing global configuration may cause a lot of failures. You can patch failing spec by adding a shared context. And here comes RSpecStamp. ## Example Use Case: Sidekiq Inline Using `Sidekiq.testing!(:inline)` may be considered a *bad practice* (see [here](https://github.com/mperham/sidekiq/issues/3495)) due to its negative performance impact. But it's still widely used. How to migrate from `inline!` to `fake!`? Step 0. Make sure that all your tests pass. Step 1. Create a shared context to conditionally turn on `inline!` mode: ```ruby shared_context "sidekiq:inline", sidekiq: :inline do around(:each) { |ex| Sidekiq::Testing.inline!(&ex) } end ``` Step 2. Turn on `fake!` mode globally. Step 3. Run `RSTAMP=sidekiq:inline rspec`. The output of the command above contains information about the *stamping* process: * How many files have been affected? * How many patches were made? * How many patches failed? * How many files have been ignored? Now all (or almost all) failing specs are tagged with `sidekiq: :inline`. Run the whole suite again and check it there are any failures left. There is also a `dry-run` mode (activated by `RSTAMP_DRY_RUN=1` env variable) which prints out patches instead of re-writing files. ## Configuration By default, RSpecStamp ignores examples located in `spec/support` directory (typical place to put shared examples in). You can add more *ignore* patterns: ```ruby TestProf::RSpecStamp.configure do |config| config.ignore_files << %r{spec/my_directory} end ``` --- --- url: /ja/guide/recipes/rspec_stamp.md --- # RSpecStamp RSpecStamp is a tool to automatically *tag* failed examples with custom tags. It *literally* adds tags to your examples (i.e. rewrites them). The main purpose of RSpecStamp is to make testing codebase refactoring easy. Changing global configuration may cause a lot of failures. You can patch failing spec by adding a shared context. And here comes RSpecStamp. ## Example Use Case: Sidekiq Inline Using `Sidekiq::Testing.inline!` may be considered a *bad practice* (see [here](https://github.com/mperham/sidekiq/issues/3495)) due to its negative performance impact. But it's still widely used. How to migrate from `inline!` to `fake!`? Step 0. Make sure that all your tests pass. Step 1. Create a shared context to conditionally turn on `inline!` mode: ```ruby shared_context "sidekiq:inline", sidekiq: :inline do around(:each) { |ex| Sidekiq::Testing.inline!(&ex) } end ``` Step 2. Turn on `fake!` mode globally. Step 3. Run `RSTAMP=sidekiq:inline rspec`. The output of the command above contains information about the *stamping* process: * How many files have been affected? * How many patches were made? * How many patches failed? * How many files have been ignored? Now all (or almost all) failing specs are tagged with `sidekiq: :inline`. Run the whole suite again and check it there are any failures left. There is also a `dry-run` mode (activated by `RSTAMP_DRY_RUN=1` env variable) which prints out patches instead of re-writing files. ## Configuration By default, RSpecStamp ignores examples located in `spec/support` directory (typical place to put shared examples in). You can add more *ignore* patterns: ```ruby TestProf::RSpecStamp.configure do |config| config.ignore_files << %r{spec/my_directory} end ``` --- --- url: /ru/guide/recipes/rspec_stamp.md --- # RSpecStamp RSpecStamp позволяет автоматически добавлять *теги* упавшим тестам (теги добавляются непосредственно в исходный код). Основная задача RSpecStamp — упрощать рефакторинг тестов. Например, при смене глобальных настроек часть тестов может упасть. Вместо того, чтобы переписывать каждый тест, мы можем пометить их специальным тегом и подключать для него shared context, сохраняющий прошлое поведение. ## Пример: миграция с `Sidekiq::Testing.inline!` на `Sidekiq::Testing.fake!` Использование `Sidekiq::Testing.inline!` в тестах по умолчанию считается *плохой практикой* (см. [здесь](https://github.com/mperham/sidekiq/issues/3495)), так как это негативно сказывается на времени выполнения тестов. Тем не менее, такая конфигурация встречается довольно часто. С помощью RSpecStamp можно быстро и без лишней работы мигрировать с `inline!` режима на `fake!`. Для этого нужно выполнить следующие шаги. Шаг 0. Для начала убедитесь, что все тесты проходят. Шаг 1. Добавьте shared context для включения `inline!` режима для конкретного теста или группы: ```ruby shared_context "sidekiq:inline", sidekiq: :inline do around(:each) { |ex| Sidekiq::Testing.inline!(&ex) } end ``` Шаг 2. Переключитесь на `fake!` режим. Шаг 3. Выполните команду `RSTAMP=sidekiq:inline bundle exed rspec`. Вероятно, часть тестов упадёт из-за изменения режима работы Sidekiq. В финальном выводе вы увидите отчёт RSpecStamp о проделанной работе: * Сколько файлов было исправлено. * Сколько изменений было сделано. * Сколько изменений не получилось применить. * Сколько файлов было проигнорировано (см. ниже). Шаг 4. Запустите тесты ещё раз — теперь они снова должны быть все «зелёными»! **Примечание**: Вы можете запустить RSpecStamp в режиме предпросмотра, указав переменную окружения `RSTAMP_DRY_RUN=1`. ## Настройки RSpecStamp по умолчанию игнорирует тесты из папки `spec/support` (где, как правило, хранятся shared examples). Вы можете добавить дополнительные паттерны для исключения: ```ruby TestProf::RSpecStamp.configure do |config| config.ignore_files << %r{spec/my_directory} end ``` --- --- url: /zh-cn/guide/recipes/rspec_stamp.md --- # RSpecStamp RSpecStamp 是一个自动为失败测试用例标记上自定义 tag 的工具。 它\_在字面上\_给你的用例加上 tag(比如,重写它们)。 RSpecStamp 的主要目标是使测试代码库重构变得容易。修改全局配置可能造成许多测试失败。你可以通过添加 shared context 为失败的 spec 打上补丁。这里 RSpecStamp 就派上用场了。 ## 范例场景:Sidekiq Inline 使用 `Sidekiq::Testing.inline!` 由于其不良的性能影响可以被看作是一种 *差* 的代码实践(原因见此 [here](https://github.com/mperham/sidekiq/issues/3495))。但其仍然被广泛使用着。 如何从 `inline!` 迁移到 `fake!`? 第 0 步,确保你的全部测试都是通过的。 第 1 步,创建一个 shared context,有条件地打开 `inline!` 模式: ```ruby shared_context "sidekiq:inline", sidekiq: :inline do around(:each) { |ex| Sidekiq::Testing.inline!(&ex) } end ``` 第 2 步,全局打开 `fake!` 模式。 第 3 步,运行 `RSTAMP=sidekiq:inline rspec`。 上面命令的输出内容包含了 *stamping* 过程的信息: * 有多少文件被影响到? * 打了多少补丁? * 多少补丁失败了? * 有多少文件被忽略了? 现在所有(或几乎所有)失败的 specs 都被打上 `sidekiq: :inline`的 tag 了。再次运行整个测试套件,检查是否还有遗留第失败测试。 也有一种 `dry-run` 模式(通过 `RSTAMP_DRY_RUN=1` 环境变量激活),打印出补丁内容而不是直接重写文件。 ## 配置 默认情况下,RSpecStamp 忽略 `spec/support` 目录下的用例(放置 shared 用例的典型位置)。 你可以添加更多的 *忽略* 模式: ```ruby TestProf::RSpecStamp.configure do |config| config.ignore_files << %r{spec/my_directory} end ``` --- --- url: /zh-cn/guide/profilers/tag_prof.md --- # Tag 分析器 TagProf 是一个根据所提供 tag 值收集测试用例分组统计数字的简单分析器。 这在结合 `rspec-rails` 的内置特性——`infer_spec_types_from_location!`——使用时很有用,可以自动添加 `type` 到测试用例的 meta 数据。 输出范例: ```sh [TEST PROF INFO] TagProf report for type type time total %total %time avg request 00:04.808 42 33.87 54.70 00:00.114 controller 00:02.855 42 33.87 32.48 00:00.067 model 00:01.127 40 32.26 12.82 00:00.028 ``` 它展示了每一组中测试用例的总数和总耗时(只百分比和平均值)。 你也可以生成一个交互式 HTML 报告: ```sh TAG_PROF=type TAG_PROF_FORMAT=html bundle exec rspec ``` 报告看起来像这样: ## 教学 TagProf 只能跟 RSpec 一起使用。 使用 `TAG_PROF` 环境变量来激活 TagProf: ```sh # Group by type TAG_PROF=type rspec ``` ## 分析 events 你可以把 TagProf 与 [EventProf](./event_prof.md) 结合使用来不仅追踪总耗时也追踪特定活动的耗时(通过事件): ``` TAG_PROF=type TAG_PROF_EVENT=sql.active_record rspec ``` 输出范例: ```sh [TEST PROF INFO] TagProf report for type type time sql.active_record total %total %time avg request 00:04.808 00:01.402 42 33.87 54.70 00:00.114 controller 00:02.855 00:00.921 42 33.87 32.48 00:00.067 model 00:01.127 00:00.446 40 32.26 12.82 00:00.028 ``` 多事件也是支持的。 ## 高级技巧:更多的类型 默认情况下,RSpec 对于默认 Rails 应用实体仅指示诸如 controllers、models、mailers 等类型。 现代 Rails 应用通常也包含了其他抽象层(比如,services,forms,presenters 等),但 RSpec 对其并不了解,也未加入任何 meta 数据。 这儿有一个变通的办法: ```ruby RSpec.configure do |config| # ... config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| # do not overwrite type if it's already set next if metadata.key?(:type) match = metadata[:location].match(%r{/spec/([^/]+)/}) metadata[:type] = match[1].singularize.to_sym end end ``` --- --- url: /guide/profilers/tag_prof.md --- # TagProf TagProf is a simple profiler which collects examples statistics grouped by a provided tag value. That's pretty useful in conjunction with `rspec-rails` built-in feature – `infer_spec_type_from_file_location!` – which automatically adds `type` to examples metadata. Example output: ```sh [TEST PROF INFO] TagProf report for type type time total %total %time avg request 00:04.808 42 33.87 54.70 00:00.114 controller 00:02.855 42 33.87 32.48 00:00.067 model 00:01.127 40 32.26 12.82 00:00.028 ``` It shows both the total number of examples in each group and the total time spent (as long as percentages and average values). You can also generate an interactive HTML report: ```sh TAG_PROF=type TAG_PROF_FORMAT=html bundle exec rspec ``` That's how a report looks like: ## Instructions TagProf can be used with both RSpec and Minitest (limited support, see below). To activate TagProf use `TAG_PROF` environment variable: With Rspec: ```sh # Group by type TAG_PROF=type rspec ``` With Minitest\*: ```sh # using pure ruby TAG_PROF=type ruby # using Rails built-in task TAG_PROF=type bin/rails test ``` NB: if another value than "type" is used for TAG\_PROF environment variable it will be ignored silently in both Minitest and RSpec. \* In Minitest 6+, you must first activate TestProf plugin by adding `Minitest.load :test_prof` in your test helper. ### Usage specificity with Minitest Minitest does not support the usage of tags by default. TagProf therefore groups statistics by direct subdirectories of the root test directory. It assumes root test directory is named either `spec` or `test`. When no root test directory can be found the test statistics will not be grouped with other tests. They will be displayed per test with a significant warning message in the report. Example: ```sh [TEST PROF INFO] TagProf report for type type time sql.active_record total %total %time avg __unknown__ 00:04.808 00:01.402 42 33.87 54.70 00:00.114 controller 00:02.855 00:00.921 42 33.87 32.48 00:00.067 model 00:01.127 00:00.446 40 32.26 12.82 00:00.028 ``` ## Profiling events You can combine TagProf with [EventProf](./event_prof.md) to track not only the total time spent but also the time spent for the specified activities (through events): ``` TAG_PROF=type TAG_PROF_EVENT=sql.active_record rspec ``` Example output: ```sh [TEST PROF INFO] TagProf report for type type time sql.active_record total %total %time avg request 00:04.808 00:01.402 42 33.87 54.70 00:00.114 controller 00:02.855 00:00.921 42 33.87 32.48 00:00.067 model 00:01.127 00:00.446 40 32.26 12.82 00:00.028 ``` Multiple events are also supported (comma-separated). ## Pro-Tip: More Types By default, RSpec only infers types for default Rails app entities (such as controllers, models, mailers, etc.). Modern Rails applications typically contain other abstractions too (e.g. services, forms, presenters, etc.), but RSpec is not aware of them and doesn't add any metadata. That's the quick workaround: ```ruby RSpec.configure do |config| # ... config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| # do not overwrite type if it's already set next if metadata.key?(:type) match = metadata[:location].match(%r{/spec/([^/]+)/}) metadata[:type] = match[1].singularize.to_sym end end ``` --- --- url: /ja/guide/profilers/tag_prof.md --- # TagProf TagProf is a simple profiler which collects examples statistics grouped by a provided tag value. That's pretty useful in conjunction with `rspec-rails` built-in feature – `infer_spec_type_from_file_location!` – which automatically adds `type` to examples metadata. Example output: ```sh [TEST PROF INFO] TagProf report for type type time total %total %time avg request 00:04.808 42 33.87 54.70 00:00.114 controller 00:02.855 42 33.87 32.48 00:00.067 model 00:01.127 40 32.26 12.82 00:00.028 ``` It shows both the total number of examples in each group and the total time spent (as long as percentages and average values). You can also generate an interactive HTML report: ```sh TAG_PROF=type TAG_PROF_FORMAT=html bundle exec rspec ``` That's how a report looks like: ![TagProf UI](/assets/tag-prof.gif) ## Instructions TagProf can be used with both RSpec and Minitest (limited support, see below). To activate TagProf use `TAG_PROF` environment variable: With Rspec: ```sh # Group by type TAG_PROF=type rspec ``` With Minitest: ```sh # using pure ruby TAG_PROF=type ruby # using Rails built-in task TAG_PROF=type bin/rails test ``` NB: if another value than "type" is used for TAG\_PROF environment variable it will be ignored silently in both Minitest and RSpec. ### Usage specificity with Minitest Minitest does not support the usage of tags by default. TagProf therefore groups statistics by direct subdirectories of the root test directory. It assumes root test directory is named either `spec` or `test`. When no root test directory can be found the test statistics will not be grouped with other tests. They will be displayed per test with a significant warning message in the report. Example: ```sh [TEST PROF INFO] TagProf report for type type time sql.active_record total %total %time avg __unknown__ 00:04.808 00:01.402 42 33.87 54.70 00:00.114 controller 00:02.855 00:00.921 42 33.87 32.48 00:00.067 model 00:01.127 00:00.446 40 32.26 12.82 00:00.028 ``` ## Profiling events You can combine TagProf with [EventProf](./event_prof.md) to track not only the total time spent but also the time spent for the specified activities (through events): ``` TAG_PROF=type TAG_PROF_EVENT=sql.active_record rspec ``` Example output: ```sh [TEST PROF INFO] TagProf report for type type time sql.active_record total %total %time avg request 00:04.808 00:01.402 42 33.87 54.70 00:00.114 controller 00:02.855 00:00.921 42 33.87 32.48 00:00.067 model 00:01.127 00:00.446 40 32.26 12.82 00:00.028 ``` Multiple events are also supported (comma-separated). ## Pro-Tip: More Types By default, RSpec only infers types for default Rails app entities (such as controllers, models, mailers, etc.). Modern Rails applications typically contain other abstractions too (e.g. services, forms, presenters, etc.), but RSpec is not aware of them and doesn't add any metadata. That's the quick workaround: ```ruby RSpec.configure do |config| # ... config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| # do not overwrite type if it's already set next if metadata.key?(:type) match = metadata[:location].match(%r{/spec/([^/]+)/}) metadata[:type] = match[1].singularize.to_sym end end ``` --- --- url: /ru/guide/profilers/tag_prof.md --- # TagProf TagProf — это специальный профилировщик для RSpec, который позволяет собирать статистику выполнения тестов (время) с агрегацией по значению выбранного *тега*. Например, если вы используете `rspec-rails` и настройку [`infer_spec_types_from_location!`](https://relishapp.com/rspec/rspec-rails/docs/directory-structure), тестам автоматически присваивается тег `type` (`controller`, `model` и т.д.). Используя TagProf, вы можете увидеть, какой тип тестов занимает больше всего времени. Пример отчёта: ```sh [TEST PROF INFO] TagProf report for type type time total %total %time avg request 00:04.808 42 33.87 54.70 00:00.114 controller 00:02.855 42 33.87 32.48 00:00.067 model 00:01.127 40 32.26 12.82 00:00.028 ``` В отчёте отображается общее число тестов и количество затраченного времени по группам, среднее время, а так же процент от всего времени прохождения тестов. Вы также можете получить отчёт в HTML формате: ```sh TAG_PROF=type TAG_PROF_FORMAT=html bundle exec rspec ``` В результате вы получите HTML с интерактивным графиком: ## Инструкция TagProf работает только с RSpec. Для активации профилировщика укажите переменную окружения `TAG_PROF`: ```sh # значение — название тега, по которому группировать тесты TAG_PROF=type rspec ``` ## Интеграция с EventProf TagProf интегрируется с [EventProf](./event_prof.md) для профилирования *событий*, а не только общего времени, затраченного на тесты. Для этого необходимо дополнительно указать переменную окружения `TAG_PROF_EVENT` с идентификатором события (или событий через запятую): ```sh TAG_PROF=type TAG_PROF_EVENT=sql.active_record rspec ``` В этом случае в отчёте появится дополнительная колонка (колонки): ```sh [TEST PROF INFO] TagProf report for type type time sql.active_record total %total %time avg request 00:04.808 00:01.402 42 33.87 54.70 00:00.114 controller 00:02.855 00:00.921 42 33.87 32.48 00:00.067 model 00:01.127 00:00.446 40 32.26 12.82 00:00.028 ``` ## Бонус: больше типов По умолчанию, `rspec-rails` добавляет тег `type` только для *классических* сущностей из Rails (controllers, models, mailers и т.д.). В современных Rails приложениях, как правило, присутствуют и другие абстракции (например, services, forms, presenters и т.д.). Для того, чтобы автоматически добавить типы всем тестам в зависимости от их расположения, мы можем сконфигурировать RSpec следующим образом: ```ruby RSpec.configure do |config| # ... config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| # если тип уже указан — пропускаем next if metadata.key?(:type) match = metadata[:location].match(%r{/spec/([^/]+)/}) metadata[:type] = match[1].singularize.to_sym end end ``` --- --- url: /ja.md --- --- --- url: /zh-cn.md --- --- --- url: /ru.md --- --- --- url: /guide/recipes/tests_sampling.md --- # Tests Sampling Sometimes it's useful to run profilers against randomly chosen tests. Unfortunately, test frameworks don't support such functionality. That's why we've included small patches for RSpec and Minitest in TestProf. ## Instructions Require the corresponding patch: ```ruby # For RSpec in your spec_helper.rb require "test_prof/recipes/rspec/sample" # For Minitest in your test_helper.rb require "test_prof/recipes/minitest/sample" ``` And then just add `SAMPLE` env variable with the number examples you want to run: ```sh SAMPLE=10 rspec ``` You can also run random set of example groups (or suites) using `SAMPLE_GROUPS` variable: ```sh SAMPLE_GROUPS=10 rspec ``` Note that you can use tests sampling with RSpec filters: ```sh SAMPLE=10 rspec --tag=api SAMPLE_GROUPS=10 rspec -e api ``` That's it. Enjoy! --- --- url: /ja/guide/recipes/tests_sampling.md --- # Tests Sampling Sometimes it's useful to run profilers against randomly chosen tests. Unfortunately, test frameworks don't support such functionality. That's why we've included small patches for RSpec and Minitest in TestProf. ## Instructions Require the corresponding patch: ```ruby # For RSpec in your spec_helper.rb require "test_prof/recipes/rspec/sample" # For Minitest in your test_helper.rb require "test_prof/recipes/minitest/sample" ``` And then just add `SAMPLE` env variable with the number examples you want to run: ```sh SAMPLE=10 rspec ``` You can also run random set of example groups (or suites) using `SAMPLE_GROUPS` variable: ```sh SAMPLE_GROUPS=10 rspec ``` Note that you can use tests sampling with RSpec filters: ```sh SAMPLE=10 rspec --tag=api SAMPLE_GROUPS=10 rspec -e api ``` That's it. Enjoy! --- --- url: /guide/profilers/tps_prof.md --- # TPSProf @available\_since version=1.6.0 TPSProf measures tests-per-second (TPS) for your top-level example groups and helps identify the slowest ones. It can also run in **strict mode** to fail the build when groups fall below a TPS threshold. Example output: ```sh [TEST PROF INFO] TPSProf enabled (top-10) [TEST PROF INFO] Total TPS (tests per second): 12.33 Top 10 slowest suites by TPS (tests per second): UsersController (./spec/controllers/users_controller_spec.rb:3) – 3.45 TPS (00:05.797 / 20, shared setup time: 00:01.203) PostsController (./spec/controllers/posts_controller_spec.rb:3) – 5.12 TPS (00:03.906 / 20, shared setup time: 00:00.876) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – 7.89 TPS (00:01.900 / 15, shared setup time: 00:00.410) ``` The output shows TPS for each group along with total time, number of examples, and shared setup time (time spent outside individual examples, e.g., `before(:all)` hooks). Groups with the highest *penalty* are shown first. The penalty is calculated as the time wasted compared to the target TPS (defaults to 30). The idea behind the *penalty* concept is to identify example groups optimizing which would bring the most time savings to your test suite, i.e., the lowest score is not necessary correspond to the lowest TPS but is affected by the total number of examples in the group. Tune your target TPS to get more accurate results. ## Instructions TPSProf currently supports RSpec only. ### Profile mode (default) Use the `TPS_PROF` environment variable to activate: ```sh # Show top-10 slowest groups (default) TPS_PROF=1 rspec # Show top-N slowest groups TPS_PROF=20 rspec ``` ### Strict mode Strict mode reports groups violating thresholds as non-example errors, making the build fail: ```sh TPS_PROF=strict rspec ``` In strict mode, configure the thresholds to report violations: ```sh # Fail groups with more than 50 examples TPS_PROF=strict TPS_PROF_MAX_EXAMPLES=50 rspec # Fail groups exceeding 30 seconds TPS_PROF=strict TPS_PROF_MAX_TIME=30 rspec # Fail groups with TPS lower than 5 TPS_PROF=strict TPS_PROF_MIN_TPS=5 rspec ``` You can combine multiple thresholds: ```sh TPS_PROF=strict TPS_PROF_MIN_TPS=5 TPS_PROF_MAX_TIME=30 rspec ``` ## Configuration ### Filtering thresholds Groups are only included in the report if they meet **all** of the following criteria: | Env var | Default | Description | |---|---|---| | `TPS_PROF_MIN_EXAMPLES` | 10 | Minimum number of examples in a group | | `TPS_PROF_MIN_TIME` | 5 | Minimum total group time (seconds) | | `TPS_PROF_TARGET_TPS` | 30 | Only groups with TPS below this value are reported | ### Report options | Env var | Default | Description | |---|---|---| | `TPS_PROF` | – | Activates the profiler. Use a number for top-N or `strict` for strict mode | | `TPS_PROF_COUNT` | 10 | Number of groups to show (overrides the `TPS_PROF` number) | | `TPS_PROF_MODE` | – | Explicitly set mode (`profile` or `strict`), overrides `TPS_PROF=strict` | ### Strict mode thresholds | Env var | Default | Description | |---|---|---| | `TPS_PROF_MAX_EXAMPLES` | – | Report groups with more examples | | `TPS_PROF_MAX_TIME` | – | Report groups with total time exceeding this value (seconds) | | `TPS_PROF_MIN_TPS` | – | Report groups with TPS lower than this value | ### Programmatic configuration You can also configure TPSProf in your test helper: ```ruby TestProf::TPSProf.configure do |config| config.top_count = 15 # Profiling thresholds config.min_examples_count = 5 config.min_group_time = 3 config.min_target_tps = 20 # Strict mode settings config.max_examples_count = 100 config.max_group_time = 60 config.min_tps = 5 end ``` ### Custom strict handler You can provide a custom strict handler to implement your own violation logic. The handler receives a `GroupInfo` object with the following attributes: `group`, `location`, `examples_count`, `total_time`, `tps`, and `penalty`. Raise an exception to mark the group as a violation. Here is an example configuration to use different TPS thresholds for different test types: ```ruby TestProf::TPSProf.configure do |config| config.mode = :strict config.strict_handler = ->(group_info) { if group_info.group.metadata[:type] == :system && group_info.tps < 5 raise "Group #{group_info.location} is too slow: #{group_info.tps} TPS" elsif group_info.tps < 20 raise "Group #{group_info.location} is too slow: #{group_info.tps} TPS" end } end ``` ## Ignoring groups and examples You can exclude specific groups from TPSProf tracking using the `tps_prof: :ignore` metadata: ```ruby RSpec.describe "SlowButOk", tps_prof: :ignore do # ... end ``` --- --- url: /guide/profilers/stack_prof.md --- --- --- url: /guide/profilers/ruby_prof.md --- --- --- url: /ja/guide/profilers/stack_prof.md --- --- --- url: /ja/guide/profilers/ruby_prof.md --- --- --- url: /guide/profilers/ruby_profilers.md --- # Using with Ruby profilers Test Prof allows you to use general Ruby profilers to profile test suites without needing to write any profiling code yourself. Just install the profiler library and run your tests! Supported profilers: * [StackProf](#stackprof) * [Vernier](#vernier) * [RubyProf](#rubyprof) ## StackProf [StackProf][] is a sampling call-stack profiler for Ruby. Make sure you have `stackprof` in your dependencies: ```ruby # Gemfile group :development, :test do gem "stackprof", ">= 0.2.9", require: false end ``` ### Profiling the whole test suite with StackProf **NOTE:** It's recommended to use [test sampling](../recipes/tests_sampling.md) to generate smaller profiling reports. You can activate StackProf profiling by setting the `TEST_STACK_PROF` env variable: ```sh TEST_STACK_PROF=1 bundle exec rake test # or for RSpec TEST_STACK_PROF=1 bundle exec rspec ... ``` At the end of the test run, you will see the message from Test Prof including paths to generated reports (raw StackProf format and JSON): ```sh ... [TEST PROF INFO] StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-total.dump [TEST PROF INFO] StackProf JSON report generated: tmp/test_prof/stack-prof-report-wall-raw-total.json ``` We recommend uploading JSON reports to [Speedscope][] and analyze flamegraphs. Otherwise, feel free to use the `stackprof` CLI to manipulate the raw report. ### Profiling individual examples with StackProf Test Prof provides a built-in shared context for RSpec to profile examples individually: ```ruby it "is doing heavy stuff", :sprof do # ... end ``` **NOTE:** per-example profiling doesn't work when the global (per-suite) profiling is activated. ### Profiling application boot with StackProf The application boot time could also makes testing slower. Try to profile your boot process with StackProf using the following command: ```sh # pick some random spec (1 is enough) $ TEST_STACK_PROF=boot bundle exec rspec ./spec/some_spec.rb ... [TEST PROF INFO] StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot.dump [TEST PROF INFO] StackProf JSON report generated: tmp/test_prof/stack-prof-report-wall-raw-boot.json ``` ### StackProf configuration You can change StackProf mode (which is `wall` by default) through `TEST_STACK_PROF_MODE` env variable. You can also change StackProf interval through `TEST_STACK_PROF_INTERVAL` env variable. For modes `wall` and `cpu`, `TEST_STACK_PROF_INTERVAL` represents microseconds and will default to 1000 as per `stackprof`. For mode `object`, `TEST_STACK_PROF_INTERVAL` represents allocations and will default to 1 as per `stackprof`. You can disable garbage collection frames by setting `TEST_STACK_PROF_IGNORE_GC` env variable. Garbage collection time will still be present in the profile but not explicitly marked with its own frame. See [stack\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/stack_prof.rb) for all available configuration options and their usage. ## Vernier [Vernier][] is next generation sampling profiler for Ruby. Give it a try and see if it can help in identifying test peformance bottlenecks! Make sure you have `vernier` in your dependencies: ```ruby # Gemfile group :development, :test do gem "vernier", ">= 0.3.0", require: false end ``` ### Profiling the whole test suite with Vernier **NOTE:** It's recommended to use [test sampling](../recipes/tests_sampling.md) to generate smaller profiling reports. You can activate Verner profiling by setting the `TEST_VERNIER` env variable: ```sh TEST_VERNIER=1 bundle exec rake test # or for RSpec TEST_VERNIER=1 bundle exec rspec ... ``` At the end of the test run, you will see the message from Test Prof including the path to the generated report: ```sh ... [TEST PROF INFO] Vernier report generated: tmp/test_prof/vernier-report-wall-raw-total.json ``` Use the [profile-viewer](https://github.com/tenderlove/profiler/tree/ruby) gem or upload your profiles to [vernier.prof](https://vernier.prof). Alternatively, you can use [profiler.firefox.com](https://profiler.firefox.com) which profile-viewer is a fork of. ### Profiling individual examples with Vernier Test Prof provides a built-in shared context for RSpec to profile examples individually: ```ruby it "is doing heavy stuff", :vernier do # ... end ``` **NOTE:** per-example profiling doesn't work when the global (per-suite) profiling is activated. ### Profiling application boot with Vernier You can also profile your application boot process: ```sh # pick some random spec (1 is enough) TEST_VERNIER=boot bundle exec rspec ./spec/some_spec.rb ``` ### Add markers from Active Support Notifications You can add more insights to the resulting report by adding event markers from Active Support Notifications: ```sh TEST_VERNIER=1 TEST_VERNIER_HOOKS=rails bundle exec rake test # or for RSpec TEST_VERNIER=1 TEST_VERNIER_HOOKS=rails bundle exec rspec ... ``` Or you can set the hooks parameter through the `Vernier` configuration: ```ruby TestProf::Vernier.configure do |config| config.hooks = :rails end ``` ## RubyProf Easily integrate the power of [ruby-prof](https://github.com/ruby-prof/ruby-prof) into your test suite. Make sure `ruby-prof` is installed: ```ruby # Gemfile group :development, :test do gem "ruby-prof", ">= 1.4.0", require: false end ``` ### Profiling the whole test suite with RubyProf **NOTE:** It's highly recommended to use [test sampling](../recipes/tests_sampling.md) to generate smaller profiling reports and avoid slow test runs (RubyProf has a signifact overhead). You can activate the global profiling using the environment variable `TEST_RUBY_PROF`: ```sh TEST_RUBY_PROF=1 bundle exec rake test # or for RSpec TEST_RUBY_PROF=1 bundle exec rspec ... ``` At the end of the test run, you will see the message from Test Prof including paths to generated reports: ```sh [TEST PROF INFO] RubyProf report generated: tmp/test_prof/ruby-prof-report-flat-wall-total.txt ``` #### Skipping test suite boot **NOTE:** RSpec only. It could be useful to exclude the application boot and tests load from the RubyProf report to analyze only tests being executed (so you don't have `Kernel#require` being one of the top slowest methods). For that, specify the `TEST_RUBY_PROF_BOOT=false` (or "0", or "f") env variable: ```sh $ TEST_RUBY_PROF=1 TEST_RUBY_PROF_BOOT=0 bundle exec rspec ... [TEST PROF] RubyProf enabled for examples ... ``` ### Profiling individual examples with RubyProf TestProf provides a built-in shared context for RSpec to profile examples individually: ```ruby it "is doing heavy stuff", :rprof do # ... end ``` **NOTE:** per-example profiling doesn't work when the global profiling is activated. ### RubyProf configuration The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers). You can specify a printer through environment variable `TEST_RUBY_PROF`: ```sh TEST_RUBY_PROF=call_stack bundle exec rake test ``` Or in your code: ```ruby TestProf::RubyProf.configure do |config| config.printer = :call_stack end ``` By default, we use `FlatPrinter`. **NOTE:** to specify the printer for per-example profiles use `TEST_RUBY_PROF_PRINTER` env variable ('cause using `TEST_RUBY_PROF` activates the global profiling). Also, you can specify RubyProf mode (`wall`, `cpu`, etc) through `TEST_RUBY_PROF_MODE` env variable. See [ruby\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/ruby_prof.rb) for all available configuration options and their usage. It's useful to exclude some methods from the profile to focus only on the application code. TestProf uses RubyProf [`exclude_common_methods!`](https://github.com/ruby-prof/ruby-prof/blob/e087b7d7ca11eecf1717d95a5c5fea1e36ea3136/lib/ruby-prof/profile/exclude_common_methods.rb) by default (disable it with `config.exclude_common_methods = false`). We exclude some other common methods and RSpec specific internal methods by default. To disable TestProf-defined exclusions set `config.test_prof_exclusions_enabled = false`. You can specify custom exclusions through `config.custom_exclusions`, e.g.: ```ruby TestProf::RubyProf.configure do |config| config.custom_exclusions = {User => %i[save save!]} end ``` [StackProf]: https://github.com/tmm1/stackprof [Speedscope]: https://www.speedscope.app [Vernier]: https://github.com/jhawthorn/vernier --- --- url: /ja/guide/profilers/ruby_profilers.md --- # Using with Ruby profilers Test Prof allows you to use general Ruby profilers to profile test suites without needing to write any profiling code yourself. Just install the profiler library and run your tests! Supported profilers: * [StackProf](#stackprof) * [Vernier](#vernier) * [RubyProf](#rubyprof) ## StackProf [StackProf][] is a sampling call-stack profiler for Ruby. Make sure you have `stackprof` in your dependencies: ```ruby # Gemfile group :development, :test do gem "stackprof", ">= 0.2.9", require: false end ``` ### Profiling the whole test suite with StackProf **NOTE:** It's recommended to use [test sampling](../recipes/tests_sampling.md) to generate smaller profiling reports. You can activate StackProf profiling by setting the `TEST_STACK_PROF` env variable: ```sh TEST_STACK_PROF=1 bundle exec rake test # or for RSpec TEST_STACK_PROF=1 bundle exec rspec ... ``` At the end of the test run, you will see the message from Test Prof including paths to generated reports (raw StackProf format and JSON): ```sh ... [TEST PROF INFO] StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-total.dump [TEST PROF INFO] StackProf JSON report generated: tmp/test_prof/stack-prof-report-wall-raw-total.json ``` We recommend uploading JSON reports to [Speedscope][] and analyze flamegraphs. Otherwise, feel free to use the `stackprof` CLI to manipulate the raw report. ### Profiling individual examples with StackProf Test Prof provides a built-in shared context for RSpec to profile examples individually: ```ruby it "is doing heavy stuff", :sprof do # ... end ``` **NOTE:** per-example profiling doesn't work when the global (per-suite) profiling is activated. ### Profiling application boot with StackProf The application boot time could also makes testing slower. Try to profile your boot process with StackProf using the following command: ```sh # pick some random spec (1 is enough) $ TEST_STACK_PROF=boot bundle exec rspec ./spec/some_spec.rb ... [TEST PROF INFO] StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot.dump [TEST PROF INFO] StackProf JSON report generated: tmp/test_prof/stack-prof-report-wall-raw-boot.json ``` ### StackProf configuration You can change StackProf mode (which is `wall` by default) through `TEST_STACK_PROF_MODE` env variable. You can also change StackProf interval through `TEST_STACK_PROF_INTERVAL` env variable. For modes `wall` and `cpu`, `TEST_STACK_PROF_INTERVAL` represents microseconds and will default to 1000 as per `stackprof`. For mode `object`, `TEST_STACK_PROF_INTERVAL` represents allocations and will default to 1 as per `stackprof`. You can disable garbage collection frames by setting `TEST_STACK_PROF_IGNORE_GC` env variable. Garbage collection time will still be present in the profile but not explicitly marked with its own frame. See [stack\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/stack_prof.rb) for all available configuration options and their usage. ## Vernier [Vernier][] is next generation sampling profiler for Ruby. Give it a try and see if it can help in identifying test peformance bottlenecks! Make sure you have `vernier` in your dependencies: ```ruby # Gemfile group :development, :test do gem "vernier", ">= 0.3.0", require: false end ``` ### Profiling the whole test suite with Vernier **NOTE:** It's recommended to use [test sampling](../recipes/tests_sampling.md) to generate smaller profiling reports. You can activate Verner profiling by setting the `TEST_VERNIER` env variable: ```sh TEST_VERNIER=1 bundle exec rake test # or for RSpec TEST_VERNIER=1 bundle exec rspec ... ``` At the end of the test run, you will see the message from Test Prof including the path to the generated report: ```sh ... [TEST PROF INFO] Vernier report generated: tmp/test_prof/vernier-report-wall-raw-total.json ``` Use the [profile-viewer](https://github.com/tenderlove/profiler/tree/ruby) gem or upload your profiles to [profiler.firefox.com](https://profiler.firefox.com). ### Profiling individual examples with Vernier Test Prof provides a built-in shared context for RSpec to profile examples individually: ```ruby it "is doing heavy stuff", :vernier do # ... end ``` **NOTE:** per-example profiling doesn't work when the global (per-suite) profiling is activated. ### Profiling application boot with Vernier You can also profile your application boot process: ```sh # pick some random spec (1 is enough) TEST_VERNIER=boot bundle exec rspec ./spec/some_spec.rb ``` ## RubyProf Easily integrate the power of [ruby-prof](https://github.com/ruby-prof/ruby-prof) into your test suite. Make sure `ruby-prof` is installed: ```ruby # Gemfile group :development, :test do gem "ruby-prof", ">= 1.4.0", require: false end ``` ### Profiling the whole test suite with RubyProf **NOTE:** It's highly recommended to use [test sampling](../recipes/tests_sampling.md) to generate smaller profiling reports and avoid slow test runs (RubyProf has a signifact overhead). You can activate the global profiling using the environment variable `TEST_RUBY_PROF`: ```sh TEST_RUBY_PROF=1 bundle exec rake test # or for RSpec TEST_RUBY_PROF=1 bundle exec rspec ... ``` At the end of the test run, you will see the message from Test Prof including paths to generated reports: ```sh [TEST PROF INFO] RubyProf report generated: tmp/test_prof/ruby-prof-report-flat-wall-total.txt ``` ### Profiling individual examples with RubyProf TestProf provides a built-in shared context for RSpec to profile examples individually: ```ruby it "is doing heavy stuff", :rprof do # ... end ``` **NOTE:** per-example profiling doesn't work when the global profiling is activated. ### RubyProf configuration The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers). You can specify a printer through environment variable `TEST_RUBY_PROF`: ```sh TEST_RUBY_PROF=call_stack bundle exec rake test ``` Or in your code: ```ruby TestProf::RubyProf.configure do |config| config.printer = :call_stack end ``` By default, we use `FlatPrinter`. **NOTE:** to specify the printer for per-example profiles use `TEST_RUBY_PROF_PRINTER` env variable ('cause using `TEST_RUBY_PROF` activates the global profiling). Also, you can specify RubyProf mode (`wall`, `cpu`, etc) through `TEST_RUBY_PROF_MODE` env variable. See [ruby\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/ruby_prof.rb) for all available configuration options and their usage. It's useful to exclude some methods from the profile to focus only on the application code. TestProf uses RubyProf [`exclude_common_methods!`](https://github.com/ruby-prof/ruby-prof/blob/e087b7d7ca11eecf1717d95a5c5fea1e36ea3136/lib/ruby-prof/profile/exclude_common_methods.rb) by default (disable it with `config.exclude_common_methods = false`). We exclude some other common methods and RSpec specific internal methods by default. To disable TestProf-defined exclusions set `config.test_prof_exclusions_enabled = false`. You can specify custom exclusions through `config.custom_exclusions`, e.g.: ```ruby TestProf::RubyProf.configure do |config| config.custom_exclusions = {User => %i[save save!]} end ``` [StackProf]: https://github.com/tmm1/stackprof [Speedscope]: https://www.speedscope.app [Vernier]: https://github.com/jhawthorn/vernier --- --- url: /guide/recipes/logging.md --- # Verbose Logging Sometimes digging through logs is the best way to figure out what's going on. When you run your test suite, logs are not printed out by default (although written to `test.log` – who cares?). We provide a recipe to turn verbose logging for a specific example/group. **NOTE:** Rails only. ## Instructions Drop this line to your `rails_helper.rb` / `spec_helper.rb` / `test_helper.rb` / whatever: ```ruby require "test_prof/recipes/logging" ``` ### Log everything To turn on logging globally use `LOG` env variable: ```sh # log everything to stdout LOG=all rspec ... # or LOG=all rake test # log only Active Record statements LOG=ar rspec ... ``` ### Per-example logging **NOTE:** RSpec only. Activate logging by adding special tag – `:log`: ```ruby # Add the tag and you will see a lot of interesting stuff in your console it "does smthng weird", :log do # ... end # or for the group describe "GET #index", :log do # ... end ``` To enable only Active Record log use `log: :ar` tag: ```ruby describe "GET #index", log: :ar do # ... end ``` ### Logging helpers For more granular control you can use `with_logging` (log everything) and `with_ar_logging` (log Active Record) helpers: ```ruby it "does somthing" do do_smth # show logs only for the code within the block with_logging do # ... end end ``` **NOTE:** in order to use this helpers with Minitest you should include the `TestProf::Rails::LoggingHelpers` module manually: ```ruby class MyLoggingTest < Minitest::Test include TestProf::Rails::LoggingHelpers end ``` --- --- url: /ja/guide/recipes/logging.md --- # Verbose Logging Sometimes digging through logs is the best way to figure out what's going on. When you run your test suite, logs are not printed out by default (although written to `test.log` – who cares?). We provide a recipe to turn verbose logging for a specific example/group. **NOTE:** Rails only. ## Instructions Drop this line to your `rails_helper.rb` / `spec_helper.rb` / `test_helper.rb` / whatever: ```ruby require "test_prof/recipes/logging" ``` ### Log everything To turn on logging globally use `LOG` env variable: ```sh # log everything to stdout LOG=all rspec ... # or LOG=all rake test # log only Active Record statements LOG=ar rspec ... ``` ### Per-example logging **NOTE:** RSpec only. Activate logging by adding special tag – `:log`: ```ruby # Add the tag and you will see a lot of interesting stuff in your console it "does smthng weird", :log do # ... end # or for the group describe "GET #index", :log do # ... end ``` To enable only Active Record log use `log: :ar` tag: ```ruby describe "GET #index", log: :ar do # ... end ``` ### Logging helpers For more granular control you can use `with_logging` (log everything) and `with_ar_logging` (log Active Record) helpers: ```ruby it "does somthing" do do_smth # show logs only for the code within the block with_logging do # ... end end ``` **NOTE:** in order to use this helpers with Minitest you should include the `TestProf::Rails::LoggingHelpers` module manually: ```ruby class MyLoggingTest < Minitest::Test include TestProf::Rails::LoggingHelpers end ``` --- --- url: /ru/guide/misc/rubocop.md --- # Копы для RuboCop TestProf включает в себя специальные копы для [RuboCop](https://github.com/bbatsov/rubocop), помогающие писать тесты более эффективно. Для подключения добавьте `test_prof/rubocop` в список загружаемых файлов в вашем конфигурационном файле: ```yml # .rubocop.yml require: - 'test_prof/rubocop' ``` Вы также можете подключить их динамически: ```sh bundle exec rubocop -r 'test_prof/rubocop' --only RSpec/AggregateExamples ``` ## RSpec/AggregateExamples Данный коп призывает разработчиков использовать специальную возможность RSpec — возможность агрегировать несколько ошибок внутри одного примера. Вместо того, чтобы слепо следовать правилу «один тест — одна проверка», можно подойти к вопросу с умом и объединить *независимые* проверки в одном примере и тем самым сэкономить время на подготовке окружения для теста. Рассмотрим пример: ```ruby # плохо it { is_expected.to be_success } it { is_expected.to have_header("X-TOTAL-PAGES", 10) } it { is_expected.to have_header("X-NEXT-PAGE", 2) } # rspec-its также поддерживается its(:status) { is_expected.to eq(200) } # хорошо it "returns the second page", :aggregate_failures do is_expected.to be_success is_expected.to have_header("X-TOTAL-PAGES", 10) is_expected.to have_header("X-NEXT-PAGE", 2) expect(subject.status).to eq(200) end ``` Данный коп помогает находить подобные примеры, а также поддерживает **автоматическую коррекцию**: примеры объединяются в один, добавляется тег `:aggregate_failures`. Если в вашем проекте настроена агрегация по умолчанию, вы можете отключить автоматическое добавление тегов указав в конфигурации RuboCop для копа `RSpec/AggregateExamples` опцию `AddAggregateFailuresMetadata: false`. **Примечание**: авто-коррекция тестов, использующих блоковые проверки (block matchers), такие как `change`, не поддерживается. --- --- url: /ru/guide/recipes/logging.md --- # Логирование в Rails Иногда анализ логов — это лучший способ определения проблемы. По умолчанию логи в Rails в тестовом режиме пишутся в файл (`test.log`) — это вряд ли нам может как-то помочь, так? Мы рекомендуем отключать логи в тестах по умолчанию (это, кстати, [увеличит их скорость на 5-6%](https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4) и использовать специальные инструменты для активация вывода логов в консоль по запросу. ## Инструкция Добавьте следующую строчку в ваш `rails_helper.rb` (или `spec_helper.rb`): ```ruby require "test_prof/recipes/logging" ``` ### Вывод логов для всего запуска тестов Для включения логирования глобально, укажите переменную окружения `LOG`: ```sh # Вывод всех логов LOG=all rspec ... # или LOG=all rake test # Показывать только логи Active Record LOG=ar rspec ... ``` ### Вывод логов для конкретного теста **Примечание:** Только для RSpec. Для вывод логов при выполнении конкретного теста укажите тег `:log`: ```ruby it "does smthng weird", :log do # ... end # или для группы тестов describe "GET #index", :log do # ... end ``` Для вывода только логов Active Record используйте тег `log: :ar`: ```ruby describe "GET #index", log: :ar do # ... end ``` ### Вспомогательные методы для логирования Для большего контроля вы можете использовать методы `with_logging` (все логи) и `with_ar_logging` (логи Active Record): ```ruby it "does somthing" do do_smth # Будут выведены логи при выполнения кода внутри блока with_logging do # ... end end ``` **Примечание:** для использования данных методов в Minitest необходимо явно добавить модуль `TestProf::Rails::LoggingHelpers`: ```ruby class MyLoggingTest < Minitest::Test include TestProf::Rails::LoggingHelpers end ``` --- --- url: /ru/guide/getting_started.md --- # Начало работы ## Требования Поддерживаемые версии Ruby: * Ruby (MRI) >= 2.5.0 (для Ruby 2.2 используйте TestProf < 0.7.0, Ruby 2.3 — TestProf ~> 0.7.0, Ruby 2.4 — TestProf <0.12.0); * JRuby >= 9.1.0.0 (некоторые инструменты требуют версии 9.2.7+). При использовании RSpec требуется версия >=3.5.0 (для более старых версий используйте TestProf < 0.8.0). ## Установка Добавьте гем `test-prof`: ```ruby group :test do gem "test-prof" end ``` Можете приступать к использованию! ## Настройки TestProf имеет ряд глобальных настроек, которые используются всеми инструментами: ```ruby TestProf.configure do |config| # путь для сохранения артефактов, например, отчётов ('tmp/test_prof' по умолчанию) config.output_dir = "tmp/test_prof" # использования уникальных имён для артефактов путём добавления временных меток в название config.timestamps = true # подсвечивать вывод config.color = true # указать устройство вывода для логов (файл или STDOUT) config.output = $stdout # или даже определить свой логгер config.logger = MyLogger.new end ``` ### Идентификаторы для отчётов/артефактов Вы также можете динамически задавать суффикс для гененируемых отчётов/артефактов с помощью переменной окружения `TEST_PROF_REPORT`. Это позволяет идентифицировать отчёты для разных запусков одного и того же профилировщика с целью последующего сравнения. **Пример.** Сравним время загрузки тестов с и без использования `bootsnap` с помощью [`stackprof`](./profilers/stack_prof.md): ```sh # Первый отчёт будет иметь суффикс `-with-bootsnap` $ TEST_STACK_PROF=boot TEST_PROF_REPORT=with-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-with-bootsnap.dump # А для второго зададим суффикс `no-bootsnap` $ TEST_STACK_PROF=boot TEST_PROF_REPORT=no-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-no-bootsnap.dump ``` Теперь у вас есть два отчётов с «говорящими» названиями! --- --- url: /ru/guide/profilers/ruby_prof.md --- # Профилирование тестов со RubyProf TestProf интегрируется с [ruby-prof](https://github.com/ruby-prof/ruby-prof) для удобного профилирования тестов. ## Инструкция Установите гем `ruby-prof` (версии >=0.17): ```ruby # Gemfile group :development, :test do gem "ruby-prof", ">= 0.17.0", require: false end ``` TestProf позволяет запускать RubyProf в двух режимах: для всего запуска тестов (`global`) или для индивидуальных тестов (`per-example`). Для профилирования выполнения всех тестов (от загрузки до завершения) укажите переменную окружения `TEST_RUBY_PROF`: ```sh TEST_RUBY_PROF=1 bundle exec rake test # or for RSpec TEST_RUBY_PROF=1 rspec ... ``` Либо программно в коде: ```ruby TestProf::RubyProf.run ``` TestProf также предоставляет специальный контекст (shared context) для RSpec для профилирования конкретных тестов. Подключить его можно с помощью тега `rprof`: ```ruby it "is doing heavy stuff", :rprof do # ... end ``` **Примечание:** индивидуальное профилирование не работает, если уже активировано глобальное. ## Настройки Наиболее часто используемая настройка — `printer` – позволяет выбирать способ вывода (*printer*) для RubyProf (см. [Printers](https://github.com/ruby-prof/ruby-prof#printers)). Вы можете указать способ вывода с помощью переменной окружения `TEST_RUBY_PROF`: ```sh TEST_RUBY_PROF=call_stack bundle exec rake test ``` Либо в коде: ```ruby TestProf::RubyProf.configure do |config| config.printer = :call_stack end ``` По умолчанию используется `FlatPrinter`. **Примечания:** для указания способа вывода при индивидуальном профилировании используйте переменную окружения `TEST_RUBY_PROF_PRINTER` (использование `TEST_RUBY_PROF` невозможно, так как оно активирует глобальное профилирование). Также вы можете указать режим RubyProf (`wall`, `cpu` и т.д.) с помощью переменной окружения `TEST_RUBY_PROF_MODE`. Все настройки доступны в [ruby\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/ruby_prof.rb). ### Фильтрация методов Иногда бывает полезно исключить некоторые методы из финального отчёта («почистить шум»). TestProf автоматически включает базовую фильтрацию из RubyProf ([`exclude_common_methods!`](https://github.com/ruby-prof/ruby-prof/blob/e087b7d7ca11eecf1717d95a5c5fea1e36ea3136/lib/ruby-prof/profile/exclude_common_methods.rb), можно отключить с помощью опции `config.exclude_common_methods = false`). Кроме того, мы так же добавляем в исключения некоторые другие популярные методы стандартных библиотек и популярных фреймворков (например, RSpec). Для отключения данной фильтрации укажите в настройках `config.test_prof_exclusions_enabled = false`. Наконец, вы можете добавить собственные исключения используя опцию `config.custom_exclusions`, например: ```ruby TestProf::RubyProf.configure do |config| config.custom_exclusions = {User => %i[save save!]} end ``` --- --- url: /ru/guide/profilers/stack_prof.md --- # Профилирование тестов со StackProf [StackProf](https://github.com/tmm1/stackprof) — это профайлер для Ruby, использующий механизм сэмплирования стеков вызовов. TestProf предоставляет удобный интерфейс для работы со StackProf. ## Инструкция Установите гем `stackprof` (>=0.2.9): ```ruby # Gemfile group :development, :test do gem "stackprof", ">= 0.2.9", require: false end ``` TestProf позволяет запускать StackProf в двух режимах: для всего запуска тестов (`global`) или для индивидуальных тестов (`per-example`). Для профилирования выполнения всех тестов (от загрузки до завершения) укажите переменную окружения `TEST_STACK_PROF`: ```sh TEST_STACK_PROF=1 bundle exec rake test # или для RSpec TEST_STACK_PROF=1 rspec ... ``` Либо программно в коде: ```ruby TestProf::StackProf.run ``` Профилирование индивидуальных тестов доступно только для RSpec, так как использует теги. Добавьте тег `:sprof` для профилирования конкретного примера: ```ruby it "is doing heavy stuff", :sprof do # ... end ``` **Примечание:** индивидуальное профилирование не работает, если уже активировано глобальное. ## Формат отчёта StackProf предоставляет CLI для работы с «сырыми» файлами отчётов, в том числе для генерации отчётов в разных форматах. По умолчанию TestProf в конце прогона тестов показывает команду, которую необходимо выполнить в терминале для формирования отчёта в формате HTML (эту команду необходимо запустить самостоятельно). Если вы хотите получить отчёт в JSON формате (например, для использования в [speedscope](https://www.speedscope.app)), вам необходимо использовать `stackprof` версии [>=0.2.13](https://github.com/tmm1/stackprof/blob/master/CHANGELOG.md#0213). Для работы с более старыми версиями вы можете использовать TestProf: укажите переменную окружения `TEST_STACK_PROF_FORMAT=json` или добавьте аналогичную опцию в настройках: ```ruby TestProf::StackProf.configure do |config| config.format = "json" end ``` ## Профилирование загрузки приложения (boot time) Время загрузки приложения в тестовом окружении также влияет на общее время выполнения тестов. Вы можете профилировать загрузку приложения с помощью StackProf с помощью следующей команды: ```sh TEST_STACK_PROF=boot rspec ./spec/some_spec.rb ``` **Примечание** Для анализа загрузки приложения мы рекомендуем использовать отчёт в формате флеймграфов. ## Настройка Вы можете поменять режим работы StackProf с помощью переменной окружения `TEST_STACK_PROF_MODE` (`wall` по умолчанию). Вы также можете изменить интервал сэмплирования с помощью переменной окружения `TEST_STACK_PROF_INTERVAL`: * для режимов `wall` и `cpu` — микросекунды (1000 по умолчанию в `stackprof`); * для режима `object` — число аллокаций (1 по умолчанию `stackprof`). Все настройки доступны в [stack\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/stack_prof.rb). --- --- url: /ru/guide/recipes/tests_sampling.md --- # Сэмплирование тестов Профилирование больших наборов тестов может занимать очень много времени. Некоторые профилировщики (например, RubyProf или StackProf) могут быть полезны и при запуске на небольшом количестве случайных тестов. Однако существующие фреймворки, RSpec и Minitest, не предоставляют возможности запускать фиксированное число случайных тестов. Поэтому мы добавили в TestProf соответствующие расширения. ## Инструкция Загрузите патч, соответствующий вашему фреймворкy: ```ruby # RSpec: добавьте в spec_helper.rb require "test_prof/recipes/rspec/sample" # Minitest: добавьте в test_helper.rb require "test_prof/recipes/minitest/sample" ``` Используйте переменную окружения `SAMPLE` для активации сэмплирования: ```sh # Запуск 10 случайных тестов SAMPLE=10 rspec ``` Вы можете также сэмплировать группы тестов, используя переменную `SAMPLE_GROUPS`: ```sh SAMPLE_GROUPS=10 rspec ``` Обратите внимание, что сэмплирование для RSpec поддерживает встроенные механизмы фильтрации: ```sh SAMPLE=10 rspec --tag=api SAMPLE_GROUPS=10 rspec -e api ``` --- --- url: /ja/guide/getting_started.md --- # はじめに ## インストール `test-prof` gem をアプリケーションに追加してください。 ```ruby group :test do gem "test-prof", "~> 1.0" end ``` これだけで完了です。TestProfを使用する準備ができました! [プロファイル](/#profilers). ## 設定 TestProf には、全ツールで使用されるグローバル設定がいくつかあります。 ```ruby TestProf.configure do |config| # レポートなどを保存するフォルダー (デフォルトは'tmp/test_prof') config.output_dir = "tmp/test_prof" # レポートに対し一意なファイル名を付与する(単に、現在のタイムスタンプを追加する) config.timestamps = true # 色付きで出力する config.color = true # ログの出力先 (デフォルト) config.output = $stdout # あるいは、カスタムのロガーインスタンスを指定することもできます config.logger = MyLogger.new end ``` また、「`TEST_PROF_REPORT`」という環境変数を使用して、レポート名に識別子を追加することができます。\ これは、異なるセットアップ間でレポートを比較したい場合に役立ちます。 **例:** `bootsnap`を使う場合と使わない場合のロード時間を[`stackprof`](./profilers/stack_prof.md)で比較してみましょう。 ```sh # 一番目のレポートに、接頭語「-with-bootsnap」を付けます $ TEST_STACK_PROF=boot TEST_PROF_REPORT=with-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-with-bootsnap.dump # bootsnapを無効にし、二番目のレポートを作成したい # Assume that you disabled bootsnap and want to generate a new report $ TEST_STACK_PROF=boot TEST_PROF_REPORT=no-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-no-bootsnap.dump ``` これで、分かりやすい名前のレポートが二つできました。 --- --- url: /zh-cn/guide/profilers/ruby_profilers.md --- # 与 Ruby 分析器一起使用 Test Prof 允许你使用通用的 Ruby 分析器来分析测试套件,而无需自己编写任何分析代码。只需安装 profiler 库并运行测试即可! 支持的 profilers: * [StackProf](https://github.com/tmm1/stackprof) * [Vernier](https://www.speedscope.app) * [RubyProf](https://github.com/jhawthorn/vernier) ## StackProf \[StackProf]\[https://github.com/tmm1/stackprof] 是 Ruby 的采样调用堆栈分析器。 确保你的依赖项中有 `stackprof` : ```ruby # Gemfile group :development, :test do gem "stackprof", ">= 0.2.9", require: false end ``` ### 使用 StackProf 分析整个测试套件 注意:建议使用[测试采样](https://github.com/test-prof/test-prof/blob/master/docs/recipes/tests_sampling.md)来生成较小的分析报告。 你可以通过设置 env 变量 `TEST_STACK_PROF` 来激活 StackProf 分析: ```sh TEST_STACK_PROF=1 bundle exec rake test # or for RSpec TEST_STACK_PROF=1 bundle exec rspec ... ``` 在测试运行结束时,你将看到来自 Test Prof 的消息,其中包括生成报告的路径(原始 StackProf 格式和 JSON): ```sh ... [TEST PROF INFO] StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-total.dump [TEST PROF INFO] StackProf JSON report generated: tmp/test_prof/stack-prof-report-wall-raw-total.json ``` 我们建议将 JSON 报告上传到 [Speedscope](https://www.speedscope.app/) 并分析火焰图。否则,请随意使用 `stackprof` CLI 来操作原始报告。 ### 使用 StackProf 分析单个示例 Test Prof 为 RSpec 提供了一个内置的 shared context,用于单独分析示例: ```ruby it "is doing heavy stuff", :sprof do # ... end ``` 注意:当全局(每个套件)分析被激活时,每个示例的分析不起作用。 ### 使用 StackProf 分析应用程序启动 应用程序启动时间也可能使测试速度变慢。尝试使用以下命令通过 StackProf 分析你的启动过程: ```sh # pick some random spec (1 is enough) $ TEST_STACK_PROF=boot bundle exec rspec ./spec/some_spec.rb ... [TEST PROF INFO] StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot.dump [TEST PROF INFO] StackProf JSON report generated: tmp/test_prof/stack-prof-report-wall-raw-boot.json ``` ### StackProf 配置 你可以通过 `TEST_STACK_PROF_MODE` 环境变量来更改 StackProf 模式(默认是`wall`)。 你还可以通过 `TEST_STACK_PROF_INTERVAL` 环境变量更改 StackProf 间隔时间。对于 `wall` 和 `cpu` 模式,`TEST_STACK_PROF_INTERVAL` 表示微秒,默认每个 `stackprof` 为 1000 。对于 `object` 模式,`TEST_STACK_PROF_INTERVAL` 表示分配,默认每个 `stackprof` 为 1。 You can disable garbage collection frames by setting `TEST_STACK_PROF_IGNORE_GC` env variable. Garbage collection time will still be present in the profile but not explicitly marked with its own frame. 你可以通过设置 `TEST_STACK_PROF_IGNORE_GC` 环境变量来禁用垃圾回收。垃圾回收时间仍将存在于配置文件中,但不会使用它自己的帧来显式标记。 请参阅 [stack\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/stack_prof.rb) 了解所有可用的配置选项及其用法。 ## Vernier \[Vernier]\[https://github.com/jhawthorn/vernier] 是 Ruby 的下一代采样分析器。试一试,看看它是否有助于识别测试性能瓶颈! 确保你的依赖项中有 `vernier` : ```ruby # Gemfile group :development, :test do gem "vernier", ">= 0.3.0", require: false end ``` ### 使用 Vernier 分析整个测试套件 注意:建议使用[测试采样](https://github.com/test-prof/test-prof/blob/master/docs/recipes/tests_sampling.md)来生成较小的分析报告。 你可以通过设置环境变量 `TEST_VERNIER` 来激活 Verner 分析: ```sh TEST_VERNIER=1 bundle exec rake test # or for RSpec TEST_VERNIER=1 bundle exec rspec ... ``` 在测试运行结束时,你将看到来自 Test Prof 的消息,其中包括生成报告的路径: ```sh ... [TEST PROF INFO] Vernier report generated: tmp/test_prof/vernier-report-wall-raw-total.json ``` Use the [profile-viewer](https://github.com/tenderlove/profiler/tree/ruby) gem or upload your profiles to [profiler.firefox.com](https://profiler.firefox.com). 使用 [profile-viewer](https://github.com/tenderlove/profiler/tree/ruby) gem 或将你的配置文件上传到 [profiler.firefox.com](https://profiler.firefox.com/)。 ### 使用 Vernier 分析单个示例 Test Prof 为 RSpec 提供了一个内置的 shared context,用于单独分析示例: ```ruby it "is doing heavy stuff", :vernier do # ... end ``` 注意:当全局(每个套件)分析被激活时,每个示例的分析不起作用。 ### 使用 Vernier 分析应用程序启动 你也可以分析应用程序启动过程: ```sh # pick some random spec (1 is enough) TEST_VERNIER=boot bundle exec rspec ./spec/some_spec.rb ``` ## RubyProf 轻松将 [ruby-prof](https://github.com/ruby-prof/ruby-prof) 的强大功能集成到测试套件中。 确保 `ruby-prof` 已安装: ```ruby # Gemfile group :development, :test do gem "ruby-prof", ">= 1.4.0", require: false end ``` ### 使用 RubyProf 分析整个测试套件 注意:强烈建议使用[测试采样](https://github.com/test-prof/test-prof/blob/master/docs/recipes/tests_sampling.md)来生成较小的分析报告,并避免缓慢的测试运行(RubyProf 有很大的开销)。 你可以使用环境变量 `TEST_RUBY_PROF` 激活全局分析: ```sh TEST_RUBY_PROF=1 bundle exec rake test # or for RSpec TEST_RUBY_PROF=1 bundle exec rspec ... ``` 在测试运行结束时,您将看到来自 Test Prof 的消息,其中包括生成报告的路径: ```sh [TEST PROF INFO] RubyProf report generated: tmp/test_prof/ruby-prof-report-flat-wall-total.txt ``` ### 使用 RubyProf 分析单个示例 TestProf 为 RSpec 提供了一个内置的 shared context,用于单独分析示例: ```ruby it "is doing heavy stuff", :rprof do # ... end ``` 注意:当全局(每个套件)分析被激活时,每个示例的分析不起作用。 ### RubyProf 配置 The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers). 最有用的配置选项是 `printer` – 它允许你指定 RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers)。 你可以通过环境变量 `TEST_RUBY_PROF` 指定 printer ```sh TEST_RUBY_PROF=call_stack bundle exec rake test ``` 或者在代码中: ```ruby TestProf::RubyProf.configure do |config| config.printer = :call_stack end ``` 默认情况下,我们使用 `FlatPrinter` . 注意:要为每个示例配置文件指定 printer,请使用 `TEST_RUBY_PROF_PRINTER` 环境变量(因为使用 `TEST_RUBY_PROF` 激活了全局分析)。 此外,你还可以通过 `TEST_RUBY_PROF_MODE` 环境变量指定 RubyProf 模式(`wall` 、 `cpu` 等)。 请参阅 [ruby\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/ruby_prof.rb) 了解所有可用的配置选项及其用法。 从配置文件中排除某些方法以仅关注应用程序代码非常有用。 TestProf 默认使用 RubyProf `exclude_common_methods!` (通过 `config.exclude_common_methods = false` 来禁用 )。 默认情况下,我们排除了一些其他常用方法和 RSpec 特定的内部方法。要禁用 TestProf 定义的排除项,请设置 `config.test_prof_exclusions_enabled = false` 。 你可以通过 `config.custom_exclusions` 指定自定义排除项,例如: ```ruby TestProf::RubyProf.configure do |config| config.custom_exclusions = {User => %i[save save!]} end ``` --- --- url: /zh-cn/guide/profilers/ruby_prof.md --- # 使用 RubyProf 进行分析 易于把强大的 [ruby-prof](https://github.com/ruby-prof/ruby-prof) 整合进你的测试套件中。 ## 教学 安装 `ruby-prof` gem (>= 0.17): ```ruby # Gemfile group :development, :test do gem "ruby-prof", ">= 0.17.0", require: false end ``` RubyProf 分析器有两种模式: `global` 和 `per-example`。 你可以使用环境变量`TEST_RUBY_PROF` 来激活全局分析: ```sh TEST_RUBY_PROF=1 bundle exec rake test # or for RSpec TEST_RUBY_PROF=1 rspec ... ``` 或在你的代码中这样写: ```ruby TestProf::RubyProf.run ``` TestProf 为 RSpec 提供了一个内置的 shared context 以对测试用例进行单独分析: ```ruby it "is doing heavy stuff", :rprof do # ... end ``` \*\*注意:\*\*在 global 被激活时,per-example 分析是无法工作的。 ## 配置 最有用的配置选项是 `printer` —— 它允许你指定 RubyProf 的 [printer](https://github.com/ruby-prof/ruby-prof#printers)。 你可以通过环境变量 `TEST_RUBY_PROF` 指定一个 printer: ```sh TEST_RUBY_PROF=call_stack bundle exec rake test ``` 或在你的代码中这样写: ```ruby TestProf::RubyProf.configure do |config| config.printer = :call_stack end ``` 默认情况下,我们使用 `FlatPrinter`。 **注意:** 要为 per-example 分析指定 printer 请使用 `TEST_RUBY_PROF_PRINTER` 环境变量(因为使用 `TEST_RUBY_PROF` 会激活 global 分析)。 你也可以通过 `TEST_RUBY_PROF_MODE` 环境变量指定 RubyProf 的模式(`wall`, `cpu`, 等等)。 参看 [ruby\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/ruby_prof.rb) 了解其所有可用的配置选项及其用法。 ### 方法排除 从分析中排除某些方法以便仅专注于应用代码是很有用的。 TestProf 默认使用 RubyProf 的 [`exclude_common_methods!`](https://github.com/ruby-prof/ruby-prof/blob/e087b7d7ca11eecf1717d95a5c5fea1e36ea3136/lib/ruby-prof/profile/exclude_common_methods.rb) (使用 `config.exclude_common_methods = false`来禁用它)。 我们默认排除了一些其他通用方法和 RSpec 特别的内部方法。 要禁用 TestProf 定义的排除,请设置 `config.test_prof_exclusions_enabled = false`。 你可以通过 `config.custom_exclusions` 来指定自定义的排除,比如: ```ruby TestProf::RubyProf.configure do |config| config.custom_exclusions = {User => %i[save save!]} end ``` --- --- url: /zh-cn/guide/profilers/stack_prof.md --- # 使用 StackProf 进行分析 [StackProf](https://github.com/tmm1/stackprof) 是用于 Ruby 的采样调用堆栈分析器。 ## 教学 安装 `stackprof` gem (>= 0.2.9): ```ruby # Gemfile group :development, :test do gem "stackprof", ">= 0.2.9", require: false end ``` StackProf 分析器有两种模式:`global` 和 `per-example`。 你可以使用环境变量 `TEST_STACK_PROF` 来激活全局分析: ```sh TEST_STACK_PROF=1 bundle exec rake test # or for RSpec TEST_STACK_PROF=1 rspec ... ``` 或在你的代码中这样写: ```ruby TestProf::StackProf.run ``` TestProf 为 RSpec 提供了一个内置的 shared context 以对测试用例进行单独分析: ```ruby it "is doing heavy stuff", :sprof do # ... end ``` \*\*注意:\*\*在 global 被激活时,per-example 分析是无法工作的。 ## 报告格式 Stackprof 提供了一个 CLI 工具对生成的报告进行操作(比如,转换为不同格式)。 默认情况下,TestProf 为你展示的命令\* 以生成分析火焰图的 HTML 报告,这样你可以自己打开它。 \* 仅当你收集了\_原生\_ 用例数据时,而这是 TestProf 的默认行为。 有时侯 JSON 报告是很有用的(比如,跟 [speedscope](https://www.speedscope.app)一起使用时),但 `stackprof` 仅自 [0.2.13](https://github.com/tmm1/stackprof/blob/master/CHANGELOG.md#0213) 版本起才支持。 如果你正在使用旧的 Stackprof 版本,TestProf 可帮助从\_原生\_数据生成 JSON 报告。对此,可使用 `TEST_STACK_PROF_FORMAT=json` 或在你代码中配置默认格式: ```ruby TestProf::StackProf.configure do |config| config.format = "json" end ``` ## 分析启动时间 应用的启动时间也会让测试变慢。尝试以如下命令使用 StackProf 来分析启动过程: ```sh TEST_STACK_PROF=boot rspec ./spec/some_spec.rb ``` **注意:** 我们推荐使用火焰图来分析启动时间,这正是为什么原生数据收集总是在`boot`模式中被开启的原因。 ## 配置 你可以通过 `TEST_STACK_PROF_MODE` 环境变量更改 StackProf 模式(默认是`wall`)。 你也可以通过 `TEST_STACK_PROF_INTERVAL` 环境变量更改 StackProf 的间隔时间。对于 `wall` 和 `cpu` 模式,`TEST_STACK_PROF_INTERVAL` 表示微秒,并且默认每个 `stackprof` 为 1000。 对于`object`模式,`TEST_STACK_PROF_INTERVAL` 表示分配,并且默认每个 `stackprof` 为 1。 参看 [stack\_prof.rb](https://github.com/test-prof/test-prof/tree/master/lib/test_prof/stack_prof.rb) 了解其所有可用的配置选项及其用法。 --- --- url: /zh-cn/guide/profilers/memory_prof.md --- # 内存分析器 MemoryProf 在测试套件运行期间跟踪内存使用情况,并可以帮助检测导致内存峰值的测试用例和测试组。内存分析支持两个指标:RSS 和分配。 输出范例: ```sh [TEST PROF INFO] MemoryProf results Final RSS: 673KB Top 5 groups (by RSS): AnswersController (./spec/controllers/answers_controller_spec.rb:3) – +80KB (13.50%) QuestionsController (./spec/controllers/questions_controller_spec.rb:3) – +32KB (9.08%) CommentsController (./spec/controllers/comments_controller_spec.rb:3) – +16KB (3.27%) Top 5 examples (by RSS): destroys question (./spec/controllers/questions_controller_spec.rb:38) – +144KB (24.38%) change comments count (./spec/controllers/comments_controller_spec.rb:7) – +120KB (20.00%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – +90KB (16.36%) change Votes count (./spec/shared_examples/controllers/voted_examples.rb:23) – +64KB (12.86%) fails (./spec/shared_examples/controllers/invalid_examples.rb:3) – +32KB (5.00%) ``` 该 examples 块显示每个示例使用的内存量,groups 块显示组中定义的其他代码分配的内存。例如,RSpec 组可能包含繁重的 `before(:all)` (或 `before_all` )设置块,因此查看哪些组在其示例之外使用最多的内存量会很有帮助。 ## 指示 使用以下命令激活 MemoryProf: ### RSpec 使用 `TEST_MEM_PROF` 环境变量设置要使用的指标: ```sh TEST_MEM_PROF='rss' rspec ... TEST_MEM_PROF='alloc' rake rspec ... ``` ### Minitest 使用 `TEST_MEM_PROF` 环境变量设置要使用的指标: ```sh TEST_MEM_PROF='rss' rake test TEST_MEM_PROF='alloc' rspec ... ``` 或使用 CLI 选项: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --mem-prof=rss # Show the list of possible options: ruby test/my_super_test.rb --help ``` ## 配置 默认情况下,MemoryProf 会跟踪使用最大内存量的前 5 个示例和组。你可以使用以下选项设置要显示的示例/组数: ```sh TEST_MEM_PROF='rss' TEST_MEM_PROF_COUNT=10 rspec ... ``` 或使用 Minitest 的 CLI 选项: ```sh # Run a specific file using CLI option ruby test/my_super_test.rb --mem-prof=rs --mem-prof-top-count=10 ``` ## 支持的 Ruby 引擎和操作系统 目前 JRuby 不支持分配模式。 由于 RSS 依赖于操作系统,因此 MemoryProf 使用不同的工具来检索它: * Linux – `/proc/$pid/statm` 文件 * macOS, Solaris, BSD – `ps`, * Windows – `Get-Process`, 需要安装 PowerShell --- --- url: /zh-cn/guide/getting_started.md --- # 快速开始 ## 安装 把`test-prof` gem 添加到你的应用: ```ruby group :test do gem "test-prof", "1.0.0.rc1" end ``` 完成!现在就可以使用 TestProf 的 [profilers](/#profilers) 了。 ## 配置 大多数 profilers 使用的 TestProf 通用配置: ```ruby TestProf.configure do |config| # the directory to put artifacts (reports) in ('tmp/test_prof' by default) config.output_dir = "tmp/test_prof" # use unique filenames for reports (by simply appending current timestamp) config.timestamps = true # color output config.color = true end ``` 你也可以通过`TEST_PROF_REPORT`环境变量动态添加 artifacts/reports 后缀。 如果你没使用时间戳并想要用不同设置生成多个报告且比较它们时,就很有用。 例如,让我们使用[`stackprof`](./profilers/stack_prof.md)来比较下测试用和不用`bootsnap`的加载时间: ```sh # Generate first report using `-with-bootsnap` suffix $ TEST_STACK_PROF=boot TEST_PROF_REPORT=with-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-with-bootsnap.dump # Assume that you disabled bootsnap and want to generate a new report $ TEST_STACK_PROF=boot TEST_PROF_REPORT=no-bootsnap bundle exec rake $ #=> StackProf report generated: tmp/test_prof/stack-prof-report-wall-raw-boot-no-bootsnap.dump ``` 现在你就有了两个带清晰名称的 stackprof 报告! --- --- url: /zh-cn/guide/playbook.md --- # 快速手册 本文档旨在帮助你开始分析测试套件,并回答以下问题:首先运行哪些配置文件?我们如何解释结果以选择后续步骤?等等。 注意:本文档假设你使用的是 Ruby on Rails 和 RSpec 测试框架。这些思想可以很容易地转化为其他框架。 ## 步骤 0:基础配置 一些显而易见的基础: * 在测试中禁用日志记录 —— 它是无用的。如果你确实需要它,请使用我们的[日志记录工具](https://github.com/test-prof/test-prof/blob/master/docs/recipes/logging.md)。 ```ruby config.logger = ActiveSupport::TaggedLogging.new(Logger.new(nil)) config.log_level = :fatal ``` * 默认情况下禁用覆盖率和内置分析。使用 env var 来启用它(例如, `COVERAGE=true` ) ## 步骤 1:通用分析 它有助于识别不那么容易实现的结果。我们建议使用 [StackProf](https://github.com/test-prof/test-prof/blob/master/docs/profilers/stack_prof.md),因此你必须先安装它(如果没有的话): ```sh bundle add stackprof ``` 默认情况下,将 Test Prof 配置为生成 JSON 配置文件: ```ruby TestProf::StackProf.configure do |config| config.format = "json" end ``` 我们建议使用 [speedscope](https://www.speedscope.app/) 来分析这些配置文件。 ### 步骤 1.1:应用程序启动分析 ```sh TEST_STACK_PROF=boot rspec ./spec/some_spec.rb ``` 注意:运行单个 spec/test 就足以进行此分析。 看到什么了?下面是些例子: * 未使用或未配置 [Bootsnap](https://github.com/Shopify/bootsnap) 来缓存所有内容(例如 YAML 文件) * 测试中不需要的拖慢 Rails 的初始化项。 ### 步骤 1.2.抽样测试分析 其思想是多次运行测试的随机子集,以揭示一些应用程序范围内的问题。你必须先启用[采样功能](https://github.com/test-prof/test-prof/blob/master/docs/recipes/tests_sampling.md): ```rb # For RSpec in your spec_helper.rb require "test_prof/recipes/rspec/sample" # For Minitest in your test_helper.rb require "test_prof/recipes/minitest/sample" ``` 然后多次运行并分析获得的火焰图: ```sh SAMPLE=100 bin/rails test # or SAMPLE=100 bin/rspec ``` 通常会发现到的: * 加密调用( `*crypt*` -任何东西):放宽其在测试环境中的设置 * 日志调用:你确定禁用了日志吗? * 数据库:也许有一些现成的经验(比如对每个测试都使用 DatabaseCleaner truncation 而非 transaction) * 网络请求:不应该用于单元测试,对于浏览器测试来说是不可避免的;使用 [Webmock](https://github.com/bblimke/webmock) 来完全禁用 HTTP 请求。 ## 步骤 2:缩小范围 对于大型代码库来说,这是一个重要的步骤。我们必须优先考虑能带来最大价值(减少时间)的快速修复,而不是单独处理复杂、缓慢的测试(即使它们是最慢的测试)。为此,我们要首先确定对整体运行时间贡献最大的测试类型。 为此我们使用 [TagProf](https://github.com/test-prof/test-prof/blob/master/docs/profilers/tag_prof.md): ```sh TAG_PROF=type TAG_PROF_FORMAT=html TAG_PROF_EVENT=sql.active_record,factory.create bin/rspec ``` 查看生成的图表,你可以确定两种最耗时的测试类型(通常是 model 和/或 controller)。 We assume that it's easier to find a common slowness cause for the whole group and fix it than dealing with individual tests. Given that assumption, we continue the process only within the selected group (let's say, models). 我们假设为整个团队找到一个常见的缓慢原因并修复它,这比处理单个测试更容易。鉴于该假设,我们仅在选定的组(例如 model)中继续该过程。 ## 步骤 3:专门的分析 在选定的组中,我们可以首先通过 [EventProf](https://github.com/test-prof/test-prof/blob/master/docs/profilers/event_prof.md) 执行基于事件的快速分析。(也许也启用了采样)。 ### 步骤 3.1:依赖项配置 此时,我们可能会发现一些配置错误或误用的依赖项或 Gem。常见示例: * Inlined Sidekiq jobs: ```sh EVENT_PROF=sidekiq.inline bin/rspec spec/models ``` * Wisper broadcasts ([需要补丁](https://gist.github.com/palkan/aa7035cebaeca7ed76e433981f90c07b)): ```sh EVENT_PROF=wisper.publisher.broadcast bin/rspec spec/models ``` * PaperTrail 日志创建: 启用自定义分析: ```rb TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_create) TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_destroy) TestProf::EventProf.monitor(PaperTrail::RecordTrail, "paper_trail.record", :record_update) ``` 运行测试: ```sh EVENT_PROF=paper_trail.record bin/rspec spec/models ``` 请参阅 [Sidekiq 示例](https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests#background-jobs),了解如何使用 [RSpecStamp](https://github.com/test-prof/test-prof/blob/master/docs/recipes/rspec_stamp.md) 快速修复此类问题。 ### 步骤 3.2:数据生成 I根据在数据库或 factories(如果有的话)中花费的时间来识别出最慢的测试: ```sh # Database interactions EVENT_PROF=sql.active_record bin/rspec spec/models # Factories EVENT_PROF=factory.create bin/rspec spec/models ``` 现在,我们可以将范围进一步缩小到生成的报告中的前 10 个文件。如果你使用 factories,请使用 `factory.create` 报表。 提示:在 RSpec 中,你可以运行以下命令自动使用自定义 tag 来标记最慢的示例: ```sh EVENT_PROF=factory.create EVEN_PROF_STAMP=slow:factory bin/rspec spec/models ``` ## 步骤 4:Factories 的使用 在测试中通过 `slow:factory` 找出最常用的 factories: ```sh FPROF=1 bin/rspec --tag slow:factory ``` 如果你看到某些 factories 的使用次数远远超过示例总数,则处理\_factory cascades\_。 可视化 cascades: ```sh FPROF=flamegraph bin/rspec --tag slow:factory ``` 可视化应该有助于确定要修复的 factories。你可以在[这篇文章](https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest)中找到可能的解决方案。 ### 步骤 4.1:Factory 默认设置 修复因模型关联而生成的 cascades 的一个选项是使用 [Factory 默认设置](https://github.com/test-prof/test-prof/blob/master/docs/recipes/factory_default.md)。若要估计潜在影响并确定要应用此模式的 factories,请运行以下分析器: ```sh FACTORY_DEFAULT_PROF=1 bin/rspec --tag slow:factory ``` 尝试添加 `create_default` 来衡量影响: ```sh FACTORY_DEFAULT_SUMMARY=1 bin/rspec --tag slow:factory # More hits — better FactoryDefault summary: hit=11 miss=3 ``` ### 步骤 4.2:Factory fixtures 回到 `FPROF=1` 结果报表,查看是否为每个示例创建了一些记录(通常为 `user` 、 `account` `team` )。考虑使用 [AnyFixture](https://github.com/test-prof/test-prof/blob/master/docs/recipes/any_fixture.md) 将它们替换为 fixtures。 ## 步骤 5:可重用的设置 在多个用例中共享相同的设置很常见。可以使用 [RSpecDissect](https://github.com/test-prof/test-prof/blob/master/docs/profilers/rspec_dissect.md) 衡量 `let` 或 `before` 所花费的时间与实际时间的比较: ```sh RD_PROF=1 bin/rspec ``` 看看最慢的组,并尝试用 [let\_it\_be](./recipes/let_it_be.md) 替换 `let/let!`,和用 [before\_all](./recipes/before_all.md) 替换 `before`。 **重要提示**:Knapsack Pro 用户必须注意,每个示例的平衡消除了使用 `let_it_be` / `before_all` 的积极影响。你必须切换到每个文件的平衡,同时保持文件较小 —— 这样才能最大限度地发挥 Test Prof 优化的效果。 ## 结论 将上述步骤应用于给定的一组测试后,你应该开发针对代码库优化的模式和技术。然后,你只需将它们推广到其他组即可。祝你好运! --- --- url: /zh-cn/guide/recipes/tests_sampling.md --- # 测试抽样 有时候在随机选择的测试上运行分析器是很有用的。不幸的是,测试框架不支持这样的功能。这就是我们在 TestProf 中包含了针对 RSpec 和 Minitest 的小补丁的原因。 ## 教学 Require 相应的补丁: ```ruby # For RSpec in your spec_helper.rb require "test_prof/recipes/rspec/sample" # For Minitest in your test_helper.rb require "test_prof/recipes/minitest/sample" ``` 然后只要添加环境变量 `SAMPLE` 带上你期望的抽样数字即可: ```sh SAMPLE=10 rspec ``` 你也可以使用 `SAMPLE_GROUPS` 变量来运行随机的测试用例组(或者 suites): ```sh SAMPLE_GROUPS=10 rspec ``` 注意,你可以把测试抽样与 RSpec filters 一起使用: ```sh SAMPLE=10 rspec --tag=api SAMPLE_GROUPS=10 rspec -e api ``` 完成。享受吧! --- --- url: /zh-cn/guide/misc/rubocop.md --- # 自定义 RuboCop Cops TestProf 自带 [RuboCop](https://github.com/bbatsov/rubocop) 的 cops 帮助你编写更有效率的测试。 要启用它们,需要把 `test_prof/rubocop` 放到 RuboCop 的配置中: ```yml # .rubocop.yml require: - 'test_prof/rubocop' ``` 根据你的需要来配置 cops: ```yml RSpec/AggregateExamples: AddAggregateFailuresMetadata: false ``` 或者你也可以动态地 require 它: ```sh bundle exec rubocop -r 'test_prof/rubocop' --only RSpec/AggregateExamples ``` ## RSpec/AggregateExamples 这个 cop 鼓励你使用近期 RSpec 一个极棒的特性——对测试用例内的失败进行聚合。 不用每个断言都编写一个用例,你可以对\_独立\_断言一起分组,这样运行所有的 setup hoods 仅需一次。 这就急剧地提升了你的性能(通过减少测试用例的总数)。 考虑这个范例: ```ruby # bad it { is_expected.to be_success } it { is_expected.to have_header("X-TOTAL-PAGES", 10) } it { is_expected.to have_header("X-NEXT-PAGE", 2) } its(:status) { is_expected.to eq(200) } # good it "returns the second page", :aggregate_failures do is_expected.to be_success is_expected.to have_header("X-TOTAL-PAGES", 10) is_expected.to have_header("X-NEXT-PAGE", 2) expect(subject.status).to eq(200) end ``` 自动纠正会一般把 `:aggregate_failures` 添加到测试用例,但如果你的项目全局启用,或通过本地文件获取 meta 数据来选择性地启用,你可以使用 `AddAggregateFailuresMetadata` 配置选项来选择不添加它。 该 cop 支持自动纠正的特性,所以你可以自动重构遗留测试! **注意**:这里展示的`its` 用例在 RSpec 3 中已经被抛弃了,但 [rspec-its gem](https://github.com/rspec/rspec-its) 的用户可以通过该 cop 来消除那种依赖。 **注意**:对于使用了 matchers 代码块的测试用例的自动纠正,比如 `change` 是故意不被支持的。 --- --- url: /zh-cn/guide/recipes/logging.md --- # 详细日志 有时侯挖掘日志是找出正在发生的事情的最佳方法。 当你运行测试套件时,默认是不打印日志出来的(尽管写到了 `test.log`——谁会去看呢?) 我们提供了一个配方以对特定的测试用例/组来开启详细日志 **注意:** 仅支持 Rails。 ## 教学 把下面这行放入你的 `rails_helper.rb` / `spec_helper.rb` / `test_helper.rb`,哪个都行: ```ruby require "test_prof/recipes/logging" ``` ### Log everything 使用环境变量 `LOG` 来启用全局日志记录: ```sh # log everything to stdout LOG=all rspec ... # or LOG=all rake test # log only Active Record statements LOG=ar rspec ... ``` ### Per-example logging **注意:** 仅支持 RSpec。 通过添加特别 tag—— `:log`来激活日志记录: ```ruby # Add the tag and you will see a lot of interesting stuff in your console it "does smthng weird", :log do # ... end # or for the group describe "GET #index", :log do # ... end ``` 要只对 Active Record 启用,使用 `log: :ar` tag: ```ruby describe "GET #index", log: :ar do # ... end ``` ### Logging helpers 对于更多粒度的控制,你可以使用 `with_logging` (记录一切)和 `with_ar_logging` (记录 Active Record)两个帮助方法: ```ruby it "does somthing" do do_smth # show logs only for the code within the block with_logging do # ... end end ``` **注意:** 要在 Minitest 使用该帮助方法,你应该手动包含 `TestProf::Rails::LoggingHelpers` 模块: ```ruby class MyLoggingTest < Minitest::Test include TestProf::Rails::LoggingHelpers end ```