Practice · SOLID · DIP · Card 1
Why is this class hard to test?
A small, focused class that does one thing. It has 12 lines of code and an unreasonable test setup. Why?
The code
A class that sends a Slack notification when a user signs up.
class NotifySignupToSlack
def call(user)
client = Slack::Notifier.new(ENV.fetch("SLACK_WEBHOOK_URL"))
client.post(text: "New signup: #{user.email}")
end
end
# The test:
test "posts to Slack" do
ENV["SLACK_WEBHOOK_URL"] = "https://hooks.slack.com/services/..."
stub_request(:post, "https://hooks.slack.com/...").to_return(status: 200)
NotifySignupToSlack.new.call(users(:karim))
assert_requested :post, "https://hooks.slack.com/..."
end The question
Twelve lines of test for twelve lines of production code, and the test is more brittle than the code. What did the production class do that made the test this annoying?
Take a moment. A class that's easy to test is usually a class that doesn't hard-code what it depends on. What did this class hard-code, and how would the test look if it hadn't?
What's hard-coded
Two things, intertwined:
- The Slack client itself. The class reaches out and instantiates
Slack::Notifierdirectly. The test can\'t hand it a fake. - The environment lookup. The webhook URL gets read from
ENVinside the method. The test has to set the env var.
Both push the responsibility for "where the dependency comes from" inside the class. The test has to wrestle with HTTP stubbing because there\'s no seam to inject a fake notifier.
Inject the dependency, drop the stub
Pass the notifier in. The class doesn\'t need to know where it came from.
class NotifySignupToSlack
def initialize(notifier: Slack::Notifier.new(ENV.fetch("SLACK_WEBHOOK_URL")))
@notifier = notifier
end
def call(user)
@notifier.post(text: "New signup: #{user.email}")
end
end
# The test, now:
test "posts to Slack" do
fake_notifier = Minitest::Mock.new
fake_notifier.expect(:post, true, [{ text: "New signup: karim@example.com" }])
NotifySignupToSlack.new(notifier: fake_notifier).call(users(:karim))
fake_notifier.verify
end No env var. No HTTP stub. No real Slack client. The test verifies the only thing that matters for this class: that it calls post with the right message. Whether the notifier is real Slack or a fake doesn\'t change the class\'s behavior, which is the point of DIP.
This is also how you switch Slack for Discord, for email, for a no-op in development — you pass a different notifier in.
Theory
For the full walkthrough, read SOLID · DIP · Dependency Injection, Config Injection, and Test Seams.