How to write good unit tests? There is no one golden answer. Today I will tray to show you a few unit testing tips&tricks.

What’s the Unit?

What is a unit test? it’s testing process of one unit. What is one unit :thinking:? A unit is the smallest part of your code you can test. What’s more important :point_right: UNIT is not a singular file :point_left:

Unit is the smallest part of your code you can test.

TIP 1 - don’t run IO operations in units

This is really important rule. You shouldn’t execute any IO operation (database connections, api calls, filesystem writing etc.) in Unit tests - they should be fast&quick&light. If we are talking about CI pipelines I would really recommend, to separate Unit Tests from Integration Tests, and run unit first. This way you will achieve fail-fast approach. Unit-tests would fail much faster than Integration-tests.

TIP 2 - do not mock everything

It’s that simple. You just don’t have to mock everything. Let me explain it by an example (Spock example, but there will be JS later):

class Foo extends Specification {
    def "should fetch a list of records"(){
        given: 
        def dependency1 = Mock(Dep1)
        def dependency2 = Mock(Dep2)
        def barService = new BarService(dependency1, dependency2)
        
        when:
        def result = barService.doSometing()
        
        then:
        result == true
    }
}

The above example illustrate the “mock everything” approach. You need to answer the question: What would you like to test? The good answerer is: business logic. Do you really need 3 tests classes to verify above logic? You need to decide. It would be good-enough to create an instance of each of these dependencies, and then test everything in each business scenario.

class Foo extends Specification {
    def "should fetch a list of records"(){
        given: 
        def dependency1 = new Dep1()
        def dependency2 = new Dep2()
        def barService = new BarService(dependency1, dependency2)
        
        when:
        def result = barService.doSometing()
        
        then:
        result == true
    }
}

Much better :smile: In the end it means that you wouldn’t create separate tests files for Dep1, Dep2 because those classes are already tested. Again, I’m not telling you that “mock” is something evil -> but just think if you really need to mock everything in your test.

Okay, so now let’s answer to following question: Is there any kind of object which must has own “test” file? Sure, but you need to ‘feel’ it. There is no golden rule. In java, you can assume that each package-private class is a good candidate to omit a separate test file.

TIP 3 - focus on business logic

This is the most important tip. I can paraphrase this to: avoid verify/toHaveBeenCalled. I know that’s sad, but it’s an excellent tip. Verify&toHaveBeenCalled are like a everyday bread, it’s so common. I think the reason is that it’s much simpler to write tests in this way. What verifying methods executions number is actually doing is putting a thousand kilograms of concrete at top of your code.
Let’s go to example:

class Bar extends Specification {
    def "should update record in database and send an email"(){
        given: 
        def service1 = Mock(Service1)
        def service2 = Mock(Service2)
        def service = new FooService(service1, service2)
        
        when:
        service.doStuff()
        
        then:
        1 * service1.updateRecord(*_)
        1 * service2.send(*_) 
    }
}

This is only one simple test, and you can say there is nothing wrong with this one. Can you tell me what have been tested here? You are not checking any results, just mock execution. A bit better approach could be to avoid void as a return type, and return something like this;

class Bar extends Specification {
    def "should update record in database and send an email"(){
        given: 
        def service1 = Mock(Service1)
        def service2 = Mock(Service2)
        repository.updateRecord(*_) >> 1
        def service = new FooService(service1, service2)
        
        when:
        def result = service.doStuff()  // (1) 
        
        then:
        result == 1 // (2)
    }
}
(1) - return business logic result

(2) - now we are checking "one record has been updated" 
      not if "repository updateRecord method has been executed once"

What has been changed? We are focus on business logic now.

You can tell that in the second example we are not executing any business logic because of mocks … I can give a simple answer: in the first example neither. What has been checked in the first version is MOCK OBJECT EXECUTION nothing more. If you are not drunk or something, and you are not going through a codebase with a scissors… the code that executes mocked logic should be there :joy:

TIP 4 - use right tools for testing things

It’s really common to see developers who would like to make 100% test coverage at all costs. He put verify or toHaveBeenCalled on EVERYTHING. In the end you’d have many tests that tests nothing, and puts tons of concrete at top of your codebase.

Wait wait wait, are you trying to tell me that verify is evil!? Nope - I’m not saying it’s evil. I’m saying that it’s easily overused. You need always look at the business - are business logic care about “application execute method A in mock B”? Not at all! Only if there is a business behind it.

We can get Angular example.

describe('FooTest', () => {

  let httpMock = jasmine.createSpyObj('HttpClient', ['get']);
  
  beforeEach(async () => {
    jasmine.clock().install();

    await TestBed.configureTestingModule({
      declarations: [FooComponent],
      providers: [
        {
          provide: HttpClient,
          useValue: httpMock
        },
      ],
    }).compileComponents();
  });
    
    it('should test rest service integration', () => {
        // given
        component.doSomething();
        
        // when
        service.sendHttpRequest()
        
        // then
        expect(http.get).toHaveBeenCalled() // don't do it like this!
    })
})

Let’s have a deeper look into example. We have a service FooService, and this service executes some HTTP request against API. The first idea could be to pass a mock through constructor, and use toHaveBeenCalled. Why not?

Because there is a better way to do it :) there is a right tool for it in Angular, called HttpTestingModule. With its help you can trace all http requests, and check what’s really happening.

describe('FooTest', () => {

    beforeEach(async () => {
       await TestBed.configureTestingModule({
          declarations: [FooComponent],
          imports: [
            HttpClientTestingModule,
          ]
        }).compileComponents();
        fixture = TestBed.createComponent(FooComponent);
        component = fixture.componentInstance;
        httpTestingController = TestBed.inject(HttpTestingController);
        httpTestingController.expectOne('/your-url').flush({ result: 'OK' });
    })
    
    it('should test rest service integration', () => {
        // given
        const http = jasmine.createSpyObj('HttpClient', ['get'])
        const service = FooService(http)
        
        // when
        def result = service.sendHttpRequest()
        
        // then
        expect(http.get).toHaveBeenCalled() // don't do it!
    })
})