Two changes here: the first is to use an awesome feature of rspec: let. Basically, we can let rspec instantiate our object, which gets all of that typing of long class names out of the way. It also lets our tests focus on what's important, and ignore what isn't. Read the tests now: "When the ip is this, I expect the locator to do that." The second thing was breaking the last test into two. I wanted to do this as a separate step, but it didn't work until I made the change: you really had two assertions in that last one. It deserves its own test.
Your original code felt the tension of two responsibilities: you have your own processing to do, as well as processing from calling an external service. You can see it in the code: you split it up into two methods for a reason. However, you marked the second as private, because you didn't want to make that part of your external API. That's great, but testing private methods is difficult, as your tests show. You're testing the private method through the public one, which is better than some Object#send shennanigans to test it directly. However, we can do better! Instead, let's make a new object, and consider it private, for our own use. We can use that object to wrap the external API, which provides two benefits: 1) clear respect of the SRP principle. One object for our stuff, one object for external processing. 2) a seam to migrate to a different service in the future. We can implement and use a different external service object, and none of our code has to change. Because we're testing through the public methods, no changes in tests are required.
The current style of the code is in a procedural style. "Here's a function, it's namespaced by my class." The standard idiom would have been to do this: ``` module Geolocator extend self def ip_lookup <snip> end ``` However, Ruby is OO, not procedural. Let's take advantage of that! Basically, you can construct an object with the arguments instead. So now, we construct a geolocator object with an IP address, and then ask it to look up the IP. Right now it might not seem like much, but imagine two or three operations on an object: ``` Geolocator.ip_lookup("123.456.789.123") Geolocator.resolve_domain("123.456.789.123") Geolocator.valid_ip?("123.456.789.123") ``` vs ``` locator = Geolocator.new("123.456.789.123") locator.ip_lookup locator.resolve_domain locator.valid_ip? ``` Much more DRY. There are also testing benefits to doing things this way: a bag of methods module is harder to test than an object.