Instantly share code, notes, and snippets.

Embed
What would you like to do?
gripes, speculations, confusion, zomg!

I was looking at a spec https://github.com/JumpstartLab/donors_choose/blob/50e4f46ba01c3565663396ccb35fd1fda3fb6772/spec/donors_choose_spec.rb and was going to make a comment about how I prefer mock objects than dynamic mocks (b/c this test doesn't fail if the dependencies change). So I went to write my own version, and realized that it is very difficult.

What I want to do is inject the mock version of Donors::Request into the DonorsChoose class. So I was thinking give it a class accessor for requester and then set it to the mock class.

Two problems here:

  1. Either the DonorsChoose::Request has a Mock::DonorsChoose::Request class that mirrors it and can handle expectations, or it is an instance of a class that can do this. If it is a class itself, then the mock is a singleton, so this one object needs to carry state that will change across different tests. If I make it an instance, then it is just confusing (the mock class can't stand in for the real class, but instead an instance of it stands in for the class... but wait, then what stands in for instances?)
  2. Either way, tests can't run in parallel because two different tests could set the requester variable to be their own mocked object, which could potentially clobber each other. Of course, it looks like the dynamic mocks should have the same issue. (Maybe make the variable be thread local?)

So I'm thinking about these things, and realize the problem is that I want to put the class into different states (its variables point to different objects) for different tests. But this is difficult because classes are unique, they are objects, sure, but they are singletons, every time I talk about that class, I'm talking about the same object.

Okay, so putting behaviour on classes sucks. But then where do I put it? Take a blog as an example. I want to find all the posts. Maybe I have a Post.all method, I want to rig it to return some value for this test, and some other value for some other test. Do I instead make a subclass for finders that I can then instantiate, something like Post::Finders.new.all ? But this doesn't solve the issue, we still get the new method being called on the singleton Post::Finders (Instead of being stuck trying to mock out the .all method, I'm stuck mocking the .new method).

I've got several other ideas, but they're all pretty extreme and highly hypothetical.

So, anyway, classes are singleton objects. That sucks, but what is there to do about it?

@JoshCheek

This comment has been minimized.

Owner

JoshCheek commented Feb 15, 2012

Might help the reader to know that I prefer static typing.

@montague

This comment has been minimized.

montague commented Feb 15, 2012

i'm sure you've put more thought into this than i have, but have you tried combining both approaches? The mock class can be defined on the fly to do different things using Class.new {...}, so you can define specialized mock 'instances' but still treat the class a a singleton. Would that work? Or am I missing something?

@JoshCheek

This comment has been minimized.

Owner

JoshCheek commented Feb 15, 2012

Like this? That might not be bad, actually.

class A
  class << self
    def with_overrides(&block)
      Class.new self, &block
    end

    attr_accessor :some_dependency

    def some_meth
      some_dependency * 2
    end
  end
end

A.some_dependency = 12
overridden = A.with_overrides { self.some_dependency = 100 }
overridden.some_meth  # => 200
A.some_meth           # => 24
@montague

This comment has been minimized.

montague commented Feb 15, 2012

yeah, this is pretty much what i was thinking. nice eloquent implementation.

@JoshCheek

This comment has been minimized.

Owner

JoshCheek commented Feb 15, 2012

Actually, I thought of a problem, because values are stored in instance variables, the overridden one won't have the parent's values if it doesn't override.

A.some_dependency = 12
A.with_overrides { }.some_meth # => 
# ~> -:10:in `some_meth': undefined method `*' for nil:NilClass (NoMethodError)
# ~>    from -:19:in `<main>'
@montague

This comment has been minimized.

montague commented Feb 15, 2012

what about starting with a clone of A, then cracking open the clone and mocking out the appropriate stuff? depending on how deep you need the clone to be, that might work.

@JoshCheek

This comment has been minimized.

Owner

JoshCheek commented Feb 15, 2012

I was actually thinking that same thing in the shower this morning, make it behave prototypical.

class A
  class << self
    def with_overrides(&overrider)
      overridden = clone
      overridden.class_eval &overrider if overrider
      overridden
    end

    attr_accessor :some_dependency

    def some_meth
      some_dependency * 2
    end
  end
end

A.some_dependency = 12
overridden = A.with_overrides { self.some_dependency = 100 }
overridden.some_meth              # => 200
A.some_meth                       # => 24
A.with_overrides.some_meth        # => 24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment