Skip to content
  • Jean Boussier's avatar
    18843235
    Implement `Object#with` · 18843235
    Jean Boussier authored
    Use case
    
    A very common pattern in Ruby, especially in testing is to save the value of an attribute, set a new value, and then restore the old value in an `ensure` clause.
    
    e.g. in unit tests
    
    ```ruby
    def test_something_when_enabled
      enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
      # test things
    ensure
      SomeLibrary.enabled = enabled_was
    end
    ```
    
    Or sometime in actual APIs:
    
    ```ruby
    def with_something_enabled
      enabled_was = @enabled
      @enabled = true
      yield
    ensure
      @enabled = enabled_was
    end
    ```
    
    There is no inherent problem with this pattern, but it can be easy to make a mistake, for instance the unit test example:
    
    ```ruby
    def test_something_when_enabled
      some_call_that_may_raise
      enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
      # test things
    ensure
      SomeLibrary.enabled = enabled_was
    end
    ```
    
    In the above if `some_call_that_may_raise` actually raises, `SomeLibrary.enabled` is set back to `nil` rather than its original value. I've seen this mistake quite frequently.
    
    Object#with
    
    I think it would be very useful to have a method on Object to implement this pattern in a correct and easy to use way.
    
    NB: `public_send` is used because I don't think such method should be usable if the accessors are private.
    
    With usage:
    
    ```ruby
    def test_something_when_enabled
      SomeLibrary.with(enabled: true) do
        # test things
      end
    end
    ```
    
    ```ruby
    GC.with(measure_total_time: true, auto_compact: false) do
      # do something
    end
    ```
    
    Lots of tests in Rails's codebase could be simplified, e.g.:
      - Changing `Thread.report_on_exception`: https://github.com/rails/rails/blob/2d2fdc941e7497ca77f99ce5ad404b6e58f043ef/activerecord/test/cases/connection_pool_test.rb#L583-L595
      - Changing a class attribute: https://github.com/rails/rails/blob/2d2fdc941e7497ca77f99ce5ad404b6e58f043ef/activerecord/test/cases/associations/belongs_to_associations_test.rb#L136-L150
    18843235
    Implement `Object#with`
    Jean Boussier authored
    Use case
    
    A very common pattern in Ruby, especially in testing is to save the value of an attribute, set a new value, and then restore the old value in an `ensure` clause.
    
    e.g. in unit tests
    
    ```ruby
    def test_something_when_enabled
      enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
      # test things
    ensure
      SomeLibrary.enabled = enabled_was
    end
    ```
    
    Or sometime in actual APIs:
    
    ```ruby
    def with_something_enabled
      enabled_was = @enabled
      @enabled = true
      yield
    ensure
      @enabled = enabled_was
    end
    ```
    
    There is no inherent problem with this pattern, but it can be easy to make a mistake, for instance the unit test example:
    
    ```ruby
    def test_something_when_enabled
      some_call_that_may_raise
      enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
      # test things
    ensure
      SomeLibrary.enabled = enabled_was
    end
    ```
    
    In the above if `some_call_that_may_raise` actually raises, `SomeLibrary.enabled` is set back to `nil` rather than its original value. I've seen this mistake quite frequently.
    
    Object#with
    
    I think it would be very useful to have a method on Object to implement this pattern in a correct and easy to use way.
    
    NB: `public_send` is used because I don't think such method should be usable if the accessors are private.
    
    With usage:
    
    ```ruby
    def test_something_when_enabled
      SomeLibrary.with(enabled: true) do
        # test things
      end
    end
    ```
    
    ```ruby
    GC.with(measure_total_time: true, auto_compact: false) do
      # do something
    end
    ```
    
    Lots of tests in Rails's codebase could be simplified, e.g.:
      - Changing `Thread.report_on_exception`: https://github.com/rails/rails/blob/2d2fdc941e7497ca77f99ce5ad404b6e58f043ef/activerecord/test/cases/connection_pool_test.rb#L583-L595
      - Changing a class attribute: https://github.com/rails/rails/blob/2d2fdc941e7497ca77f99ce5ad404b6e58f043ef/activerecord/test/cases/associations/belongs_to_associations_test.rb#L136-L150
Loading