“It’s more about good enough than it is about right or wrong.”
James Bach
Disclaimer: Before I start talking about adding automatic tests into the project using Jest, I need to point out two things:
- I’m not a Javascript developer. I feel far more comfortable with PHP, Java, or almost any OOP language. So keep in mind that this is not a “You should do it like this” but more of an “I did it like this, how can I improve?”-kind of article.
- I chose the quote above for a reason. There are certain methods in this code that are either unnecessary to be tested or far easier to test them with a (manual) functional test. So the aim is not to achieve 100% code coverage or a ‘perfect’ codebase. The goal is to add testing to feel more confident about my code and detect possible bugs.
With that being said, let’s grab a coffee and talk about: Testing.
Why automated testing?
We already saw in the first two parts of the series that our code is working and successfully adding time bookings by mapping Google Calendar events with a .csv
file. Thus the question comes to mind: why should we even bother writing automated tests?
There are several good reasons why you should write unit tests, and a lot of brilliant people have written about it! I will link some excellent articles at the end of this post.
But here are some of my key points:
- Automatic tests save time because you don’t need to manually check your whole workflow/application when you’ve changed just a small piece of code.
- Automatic tests point you towards architectural flaws in your code.
- Automatic tests give you confidence that the part that you tested is working.
- Automatic tests can ensure that you didn’t break functionality when changing your code.
- Automatic tests have a documentation function. When you have a specific bug or edge case, you can write a test case for this particular situation to let others know about this specific behavior.
Additionally to those key points, I have the feeling that a lot of beginning programmers don’t know about the fantastic benefits of automatic testing or they want to learn it but don’t know how to start.
How to start?
In Javascript, writing tests is relatively easy when you decided on which testing framework you want to pick. There is Mocha, Karma, Jasmine, Pupeteer, Jest, AVA, Tape, QUnit, Chai, and even more!
While I’ve been working with Mocha and Jasmine before, I decided to pick Jest this time. Jest is developed by Facebook and has gained a lot of reach by its combination with ReactJS. For this reason, I am fairly confident that this testing framework will be maintained and used for quite some time and won’t be abandoned during the next one or two years.
npm install --save-dev jest
While Jest is merely looking for files ending with .test.js
, I feel it gives a better overview to move all test files into /tests
and the business logic which is currently located at /
into /src
.
What should I test?
Or rather, what should I not test?
There are functions that don’t need testing. Usually, those are getter and setters (as they don’t hold any logic), as well as some creational methods (in our code base, for example, getTimeMax()
, which is currently only creating a new Date
object). Now I know, this is already worth a discussion. You could argue that getTimeMax()
might be changed in the future, and the change could break other functionality. If you choose this or that path really depends on the project, your personal preference, your confidence in your codebase, or the rules your team agreed upon.
For this small side project in the current situation, I’ve decided not to test several methods. For example, I will not write unit tests for Google authentication or how to get the mappings from a .csv-file into an array of objects. Essentially, I’m not going to test methods that are only calling a third-party API as well as I am not testing the third-party library itself.
Testable code
If you haven’t checked out part 1 and part 2 of the series, now might be the perfect time. For those who have I will quickly recapitulate:
- In part 1, I scribbled down an idea in Javascript so we can get Google calendar events, map them with a set of predefined project- and service-ids and create time tracking entries for Mite.
- In part 2, I cleaned up the code so that we now have separate classes with different responsibilities instead of 2 files with quick a lot of code without a proper structure.
Because we did all this preparatory work in part 2, we can now start testing (and mocking) classes. From my experience, this is where a lot of ambitious programmers fail because they want to write tests for code that has grown over the years, has far too many dependencies (and by that usually too many responsibilities, also) and often is not in a testable state. It doesn’t mean that you cannot test it at all, but it makes testing extremely hard and very difficult for beginners. For this reason, I like to add unit tests to a project as soon as possible because (see Key Point 2):
Automatic tests point you towards architectural flaws in your code.
Dependency Injection
Coming from a PHP/Java background, I am very used to injecting all dependencies into the classes by some kind of DI container. The reason why this is more commonly used in languages like C++, Java, or PHP than in JS projects is that it is absolutely needed there. In Javascript, you can survive without using Dependency Injection, and that’s why I won’t use it right now. But if you want to read more about it, I can recommend these two articles for you:
https://tsh.io/blog/dependency-injection-in-node-js/
Let’s start testing!
After deciding which of the service won’t be tested, there are only three classes left that need testing: Mite.js
, DateTimeHelper.js
, EventMapper.js
. Starting with the tests for getProjectAndServiceMapping
from EventMapper
, there are two scenarios I want to automatically test:
- Successfully finding a mapping for a calendar entry returns
[projectId, serviceId]
. - Not finding a mapping returns
[null, null]
.
const EventMapper = require('../src/EventMapper.js'); describe('getProjectAndServiceMapping', () => { test('should get correct project and service mapping', () => { const mockMappings = [ {keyword: '#321', projectId: 0, serviceId: 0}, {keyword: '#123', projectId: 1234, serviceId: 4321}, {keyword: '#456', projectId: 9876, serviceId: 5432} ]; const [projectId, serviceId] = EventMapper.getProjectAndServiceMapping(mockMappings, 'summary #123'); expect(projectId).toBe(1234); expect(serviceId).toBe(4321); }); test('should return array with two null values', () => { const mockMappings = []; const [projectId, serviceId] = EventMapper.getProjectAndServiceMapping(mockMappings, 'summary #123'); expect(projectId).toBe(null); expect(serviceId).toBe(null); }) });
You’ll notice for the first test that I added two additional mappings. This is used to make sure that the method is not somehow taking the first or last mapping for every entry but precisely the one keyword that is included in the calendar entry title. As mentioned before, I decided against testing getMappings
right now because it only reads a .csv
file into an array of objects.
DateTimeHelper.js
For DateTimeHelper.js
there are two methods we should test, and thus I added a describe
block for both of them. Through this, we get a nicely readable description of our tests when running our tests (for example) with PhpStorm:
const DateTimeHelper = require('../src/DateTimeHelper.js'); describe('formatDate', () => { test('should return date as YYYY-MM-DD', () => { let input = '2020-01-01T00:00:00-02:00'; let result = DateTimeHelper.formatDate(input); expect(result).toBe('2020-01-01'); }); }); describe('getDurationInMinutes', () => { test('should return 60 minutes', () => { let start = new Date(); let end = new Date(start.getTime() + (60 * 60000)); let result = DateTimeHelper.getDurationInMinutes(start, end); expect(result).toBe(60); }); });
Additionally, we could test what happens if the input parameters are not Date
objects as expected, but the actual input comes directly from Google Calendar entries, which should always return a start and end date.
Mite
Two tests are done, one more to go, and we haven’t even used Mocks yet! But for Mite.js
we need the mock capabilities of Jest now. The reason is that Mite.js
is using the Config.js
file as well as the EventMapper.js
to create the correct entry format to save the entries in Mite. Instead of Dependency Injection, we are currently only using require
to load dependencies. So we cannot inject a fake implementation into the class, and therefore we would always get different results depending on your personal config values and mappings. We would have a hard time reproducing failing tests like this, so instead, we use jest.mock
to replace the actual implementation with a fake one.
jest.mock('../src/Config', () => ({ MITE_API_KEY: 'apiKey', MITE_ACCOUNT: 'miteAccount', INPUT_PATH: 'inputPath', GOOGLE_AUTH_DIRECTORY: 'authDirectory' })); jest.mock('../src/EventMapper', () => ({ getProjectAndServiceMapping: () => [123, 456] }));
Now we know exactly which values will be used to generate the Mite entry format. All we have to do now is faking a Google Calendar entry (event
) and initialize it with the necessary parameters.
let start = new Date('2020-01-01T00:00:00-02:00'); let end = new Date(start.getTime() + (60 * 60000)); const event = { start: {dateTime: start}, end: {dateTime: end}, summary: 'summary' };
All that is left to do is testing if the actual result is the same as we expect it. We could either check every parameter separately or use deep matching with Jest’s toStrictEqual
assertion. Here is the complete test file:
const Mite = require('../src/Mite'); jest.mock('../src/Config', () => ({ MITE_API_KEY: 'apiKey', MITE_ACCOUNT: 'miteAccount', INPUT_PATH: 'inputPath', GOOGLE_AUTH_DIRECTORY: 'authDirectory' })); jest.mock('../src/EventMapper', () => ({ getProjectAndServiceMapping: () => [123, 456] })); test('generateEntryFormat return values', () => { let start = new Date('2020-01-01T00:00:00-02:00'); let end = new Date(start.getTime() + (60 * 60000)); const event = { start: {dateTime: start}, end: {dateTime: end}, summary: 'summary' }; let entry = Mite.generateEntryFormat(event, []); const expectedResult = { date_at: '2020-01-01', minutes: 60, note: 'summary', project_id: 123, service_id: 456 } expect(entry).toStrictEqual(expectedResult); })
Conclusion
As I mentioned in the beginning, this is probably not the best solution, but this is where you come into play! Please comment, discuss and contribute to the repository so we can learn from each other and learn about different solutions and best practices:
https://github.com/moritzwachter/calendar-to-mite/tree/0.4
And as I promised…
Here are some link and book recommendations for you:
https://dzone.com/articles/top-8-benefits-of-unit-testing
https://www.codemag.com/Article/1901071/10-Reasons-Why-Unit-Testing-Matters
https://thenewstack.io/unit-testing-time-consuming-product-saving/