DEV Community

Daniel Gomez
Daniel Gomez

Posted on • Edited on

Test Code Sugaring - Using types and code extensions to improve code expressiveness

All of this is applicable of any typed language, I used it in android with Kotlin in Android and now Im happy to use them in Flutter also (the at the bottom of the post to see an example of how you can do it on android and on dart).

Without types (be sad)

Lets assume that you are testing a presenter like this:

class LoginPresenter {
    final userRepository = UserRepository();

    void onLogin(String email, String password) async {
        final response = userRepository.login(email, password);

        if(response.httpCode == 200) {
            // Parse the response body and do something with it
        } else {
           // parse the response body to get the errors
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That is using some abstractions like this ones:

class Response {
    String body;
    int httpCode;
}

class UserRepository {
    Future<Response> login(String email, String password) { 
      // Do the Http call here and return the http response
    }
}
Enter fullscreen mode Exit fullscreen mode

How your tests may look

Using mockito to create the mocks:

void main() {
    test('on success', () {
       whenever(userRepository.login(any, any)).thenAnswer(() => Future.value(Response("your big json response", 200)));
       presenter.onLogin("email", "password");

       // expect each thing based on the json response values
    }) 
}
Enter fullscreen mode Exit fullscreen mode

Problems with this

The problems with this kind of tests are:

  • You should be testing only 1 thing in a unit test, with this, you are testing the JSON parsed object, think the case that the service you are using changed the representation of the response to something else, you would have to go and change every BIG json string file and pray :P.
  • There are no so much separation of concerns since the presenter know what you are using Http to fetch the data, also know that 200 is a success, what if the service starts answering with 201?
  • You will end up with big Json strings representing each possible state that the presenter will handle and that your service may answer. You will suffer maintaining this JSONs over the time.
  • If you have to mock multiple repositories it is a little bit fill of boilerplate like returning a Future.value that is inside a function () => ... and the 200 again everywhere.
  • The presenters should parse the JSON file to get the errors and handle each case, and sometimes it requires you to use the http code together.

All of this is a lot of irresponsibility for a presenter doesn't it? when something does a lot of things then testing and maintaining those tests is a hell really hard.

Adding types for the rescue!

You can add some types to your system to represent the Success and the Failure of any task everywhere.

You can add some simple abstractions:

abstract class Result<T> {
}

class Success<T> extends Result<T> {
  final T body;

  Success(this.body);
}

class Failure<T> extends Result<T> {
  final String cause;

  Failure({this.cause});
}
Enter fullscreen mode Exit fullscreen mode

I

And a repository that returns responses with Result class, like:

class Session {
   int userId;
}

class UserRepository {
    Future<Result<Session>> login(String email, String password) { 
      // Do the Http call here and get the user id...
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the presenters will look a cleaner:

class LoginPresenter {
    final userRepository = UserRepository();

    void onLogin(String email, String password) async {
        final result = userRepository.login(email, password);

        if (result is Success<Session>) {
           // use result.body.userId been sure that it is an int
        } else if (result is Failure<Session>) {
           // use result.message been sure that theres a string with the error
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Goods of this

  • You remove a lot of knowledge from presenters, they will not know anything about Http, http codes or json parsing.
  • You defined types for responses an so presenters can be sure that the response is a Session that have an int attribute with the userId, so no double check or casting from string to the type you are supposed to get.

How tests benefits from this

Now that you know that your repositories responses your types, you can add extensions like this:

extension ResultMock<T> on PostExpectation<Future<Result<T>>> {
  // One for success
  void thenSucceed(T body) {
    var result = Result.success(body);
    thenAnswer((realInvocation) => Future.value(result));
  }

  // One for failures
  void thenFail([String message = 'mock-error']) {
    var result = Failure(message);
    thenAnswer((realInvocation) => Future.value(result));
  }
}
Enter fullscreen mode Exit fullscreen mode

Then the tests may look like:

void main() {
    test('on success', () {
       whenever(userRepository.login(any, any)).thenSucceed(Session(1));
       presenter.onLogin("email", "password");

       // expect each thing based on the json response values
    }) 
}
Enter fullscreen mode Exit fullscreen mode

What you get from this

  • Tests are testing only one thing and that's the behaviour of the presenter.

  • No need to maintain JSONS everywhere, you can add tests in the repository to test the parsing if that's what you want to, but they will be in only one place of your system.

  • You will mock easily everywhere since you don't need to think that you need to create a Future, a lambda that answers that future and the proper result type, just use the extension with the response type.

  • Some people prefer to use fake implementations of their abstractions to "mock responses", that is not the topic of this post to' I believe that with this you may not need to create fake implementations, mocking became so easy with this...

  • This can be used on any typed language, here are some examples of dart and kotlin extensions if you come from android and want to try it:

Types are a really powerful thing to separate concerns of your system.

Top comments (1)

Collapse
 
enzoftware profile image
Enzo Lizama Paredes

Awesome article. Really nice concepts explained easily. Love it <3