mardi 3 septembre 2019

What to unit test in an API client

I've been reading about unit testing day in, day out for a few days and the more I did, the more frustrated I became.

The System Under Test is essentially a client calling external APIs that take XML parameters and generate documents. It's legacy code and I'm trying to write tests for it.

A simplified version of this class would look similar to this

class DocumentService
  def create_document(item, buyer, seller, other)
    payload_hash = {}
    # 1. format API request payload using item, buyer, seller, other
    payload["abcd"] = item.abcd;
    ...
    payload = Gyoku.xml(payload_hash)

    # 2. call external API
    response = RestClient.post(@@external_url, payload)

    # 3. handle the response
    case response.code
    when 200
      # some processing logic here
      return GeneratedDocument.new(
        document_id: document_id,
        document_pdf: document_pdf,
      )
    else
      fail "Failed"
    end
  end
end

The First part generates the payload.

  1. I'm not sure whether/how to test if the correct payload is generated from the parameters passed(item, buyer, seller).
    I've thought about extracting this into generate_payload(item, buyer, seller) function and passing Fake item, buyer, seller objects, but doesn't that amount to testing a private function?
    Where should this effect be sensed and tested?

The Second part makes a POST request using the payload.

  1. I guess, if I Fake the post request call on the Second part, I could test the payload from there. Something like this:
class DocumentService
  def create_document(item, buyer, seller, other)
  ...
  response = call(@@external_url, payload)
  ...
  end

  def call(url, payload)
    RestClient.post(url, payload)
  end
end

class FakeDocumentService < DocumentService
  @payloads = []  
  def call(url, payload)
    # do nothing
    @payloads.push(payload)
  end
end

def test_payload
  fakeDocumentService = FakeDocumentService.new
  # instantiate fake objects
  fakeDocumentService.create_document(fakeItem, fakeBuyer, fakeSeller, fakeOther)
  expect(fakeDocumentService.@payloads[0]).to eq (expected_payload)
end

However, this feels like an indirect way to test the payload logic.

A bigger issue is that, since the call is a command (as in command query separation), I feel like the call needs to be Mocked than Faked to be tested appropriately.

Should and can I mix Fake and Mock by testing the payload generation part using a Fake and testing if correct endpoints are called by Mocking? (A popular article (https://martinfowler.com/articles/mocksArentStubs.html) seems to suggest that you are either a Classicist or a Mocist and you need to choose to either Mock or Fake, which frustrates me more.)

The Third part processes the result from the external API.

  1. How should I test this? Should I Stub the API call to return different types of responses and test how they're handled? If so, I'll be Faking the external call to test the First part, Mocking it to test the Second part, and Stubbing it to test the Third part. Am I doing this right?

  2. Am I trying to test too much? I saw how Stripe tests their Ruby client (e.g. https://github.com/stripe/stripe-ruby/blob/master/test/stripe/customer_test.rb) and it looks like they are only testing whether a request to the correct url is made. This may be the payloads to their calls is not as lengthy and complicated as ours, and they are making calls to their own service, whereas our code makes calls to APIs of other companies.

I have so many other questions but these are some of the big questions.

Aucun commentaire:

Enregistrer un commentaire