How to face up with the bugs?
10 Testing Tips for Developers
Make less error-prone software!
Planning is everything, the plan is nothing!
— Dwight D. Eisenhower
If you’re planning to design your system and do all choose fancy diagrams( Data Flow Diagram, Use Case, JSP Diagram, Statechart, etc.), you should then prepare for bugs. It’s impossible to design a perfect system which scales perfect and has zero corner cases.
I’m going to give you 10 tips which I have learned in a hard way!
1. Think about co-location
I have seen once an unusual way of code commenting. At first, it looks silly, but it can make sense. The only guy was maintaining that whole system… He separated code comments from code. He moved all his code comments into a separated folder called
docs. It was something like this:
I’m not a fan of that approach. There would be some serious problems. I listed out three biggest:
- maintainability- forgot to update the corresponding
- nontransparent - programmer looking at the code and it cannot figure out what the heck is going on;
- waste of time - thinking about those all location which you have to update, etc.) we’d encounter by not co-locating our comments with the codebase.
The same applies to tests. The concept of sticking tests makes your life easier. You are going to avoid most of the problems that the previous approach will have. So, hold your
__tests__ folder with the group of code you’re testing. I’ll show you an example of one of my newest project:
For me, it’s quite common to take a TDD approach (which wasn’t utilised by our project because of the nature of the Startup…).
Conclusion: Make a folder called
__tests__ within the folder where’s the code which are you going to test.
2. Don’t ever ignore Internationalization Testing
According to the last Ethnologue’s research, 7117 languages are spoken today at the time of writing this Medium’s story. That number is continually in flux because we’re learning more about the world’s languages every day.
So if you want to make a product or service which fits a bunch of markets, you have probably take care of internationalization.
If someone asks me: ‘What is the internationalization testing?’ I would probably say a set of techniques which help us to make our product fit into multiple regions and cultures.
You aren’t forced by anyone to write those test on your own. There is a bunch of testing library. You would probably struggle with this problem on Front End, so I’ll recommend one cool library which I used once: RTL for CSS in JS.
3. Monkey patching can be a tricky
What about monkey patching? Well, it’s not an optimal solution for our modules, but sometimes we cannot avoid it. The main reason for using mocking is to prevent third-party service to work. For example, if we’re using Twilio SendGrid Email API, we don’t want to send 100 emails in a time interval of 10 seconds. Service will charge us for that, and we’re wasting resources unnecessarily.
I’ll give a basic example:
A problem can be for example if we call
randPicker(12, 1) too many times in the scope of the test function, or if we call it in the wrong way, then the things can blow up.
When you’re designing a fake version of something (mocking) in your test(s), you’re cutting any faith which you have in the integration between the mock and the logic you’re testing at the moment. Let’s see an example:
We finished our work and left the project to our colleagues. He decided to add a new feature. College is a writing test, and he chose to fix this mock function:
Colleague add a new argument
maxArgs… Our mock was modified, the test is still passing, and code is busted! In the end, I’m not persuading you to stop mocking. You have to be careful when you’re using it. I’m my opinion, I’m not using it always, but sometimes I have to.
If you’re working with sensitive data (user info, credit card information, etc.), there is no around way. You’re forced by the situation to use it. There is no way of knowing if you’re appropriately calling things. In this situation, using a Mocking Strategy is highly recommended by Sr. QA.
There are situations when you’re testing toll service, and you cannot charge a user’s account just for testing purpose. By the way, testing the compatibility of two separated systems is called Contract Testing.
P.S. Unfortunately, you cannot fake your bank account with mocking, so then you can obtain a bunch of money!
4. Respect the idea of the pure function
I highly recommend to get familiar with Linter and use this tool appropriately. I’ll show you one of the most influential Linter configurations when you’re using The Mocking Strategy.
In the next example we have a
.eslintrc which disables a linting rule called
This is the
.eslintrc import plug-in. If I turn this back on (change
warn), it ensures that you don’t make changes to anything that you import, at least not on the namespace.
If you want to know why this rule exists, we should then talk a little bit about the history of NodeJS. When ECMAScript’s Modules were implemented in Node.js, and if we’re using native modules, this wouldn’t work. It may produce a syntax error or (at least) a runtime error.
That is an extra reason why you need to think twice when you want to mock your data.
5. Frontend as a prototype tool
If you’re working on a big business system, it’s pretty normal to design the system first. Based on the system design, you will implement the server-side. Lastly, do the stuff on the frontend.
In the Startup industry, it’s not uncommon to prototype frontend first so you can express your idea via visual content. The common question you’ll ask is: How to test if using internal API which is not ready at the moment?
That’s a great question.
You’re considering using Mock, aren’t you? That is related to the case where you have the backend implementation. It depends on the piece you’re testing. Maybe, you’re doing an integration test which integrates two or more services (contract testing), or you’re trying to verify network request, etc.
Sometimes you can rely on those services, primarily if the services are notable. In that case, you might not want to have a mock. But, if you’re doing a Unit test (low-level tests), you have to have mock.
You have to adopt the right mental model. If your internal API is not ready yet, then it’s nice to have mock. Not in the context of testing, but in the context of being able to develop the software. It’s a good testing strategy, in my opinion.
So, if your test is going to use API when the service isn’t available, you have to use mock. It’s nice to test your service, but if you’re running the test on a local machine, which I do most of the time, I want to be able to test controllers, so I mock it firstly.
Conclusion: It’s nice to have a mock version of the third-party service because you might run the tests when you’re offline, or when internal API is not ready yet.
6. Be explicit as much as you can
Let’s consider we are testing INDEX Users route:
We’ll write the corresponding test:
The test will pass, of course. If we, for example, change it to
toHaveBeenCalled(3) it’s going to fail because it expects to be called by test once. There is another assertion which we can use, that’s
The reason why I don’t use it personally is that I want to be more explicit here about how many times was called by test. Why? Sometimes, you can make a mistake in a controller. For example, you can write the same function call twice:
The test is going to pass with
toHaveBeenCalled(), but not with
toHaveBeenCalledTimes(1). Be explicit on how many time things respond, to avoid similar situations. In the end, you can verify what was called by
7. It’s tough to write a pure Unit test
The previous example is not a pure unit test. A unit test is one that mocks all dependencies. It only tests the things which are within the corresponding module. If we want to have a pure unit test, we will provide a mock for everything which are we importing (utils, database, etc.). We’re focusing on that unit.
Conclusion: If you prefer a pure unit test, you’ll have to mock everything. Otherwise, you won’t have a pure unit test.
I don’t care much about differentiating between unit and integration tests. I mean, I don’t care about those names, what I care about is the confidence which I’m getting out of tests. I almost forgot I care about extra work when I have to do all those mocking things too.
NOTE: Don’t neglect to think about writing additional tests to acquire more reliance on doing things which are working together correctly.
8. Initialize the database before every test
Before every single one of my tests, I always initialize the database on the asynchronous way. I’ll spend some time on initialization then I’ll run tests.
It’s a practice I highly recommend because there can be a situation where you’re working with a big database which is slowing down your tests. That’s a worst-case scenario - time-wasting.
Yet another peculiarity is to earn a bit more confidence because you are sure that the test is running in isolation, independently from other test cases. If the test fails, and your database is in a complete disorder, you should operate the initialization process again.
If you have a big application which requires to have a big database, you’ll initialize the database once, before tests, and you won’t slow down the system. That is a good strategy in the case of large amounts of data, at least in my opinion.
In more modest applications, you can apply fixtures which is the same across all test. You’ll know what should be in the database at any given time. After every test, you should clean your database.
You may be wondering should we consider perform the unit tests in complete isolation?
Well, I hope you are wondering, and I am not writing an answer in vain. I don’t care about the units being tested in the isolation. All I care is about the validity of the tests. I believe the manner of slicing all the dependencies of the testing unit will ensure the test to be more reliable but personally do that seldom.
If I have a sufficiently complex unit and initializing the database before every test (to test all complex business logic of that unit) is a waste of time, I’ll rip it out and put it into a pure function. I solved the time complexity problem, and I could test in isolation all day long.
That’s a great way to get the best of both worlds. I can get the momentum of the pure unit tests without sacrificing a lot on confidence. You can have one test which is validating business logic, and a bunch of unit tests around that which are pure functions.
There are situations were dropping down to a lower level of testing is optimal. You can have something that right now you can only test as kind of an integration test. You pull out the piece that has a lot of edge cases which is handling then you put that into its little module and then you can test that all out.
Conclusion: There isn’t an ideal solution. You have to weight consequences either way. But, cleaning the database after every test will give you a vast amount of confidence. You have to have on mind that this can consume more time.
9. Use Test Object Factories
Let’s consider the next case:
When most of the request object looks the same as the other test, you should use Test Object Factory (a.k.a. ObjectMother). The theory is that you take the common stuff and put it in a function, then as needed, you take back whatever you need.
It’s common to name this function a
setup(). The function is responsible for whatever kind of set up, which is the same across all tests. You are not restricted to use the same setup. You can always add some custom things if you need it.
You could do stuff like using
beforeEach(), but that’s not a great path if you’re interested in what’s going in your tests.
10. What is a target for TDD?
Test-Driven Development has a pretty good place in the world of pure functions. If nothing else, it’s easier to examine the pure function.
There a lot of places when we can use a TDD approach. A circumstance where we’re appending new features, deleting, etc., are high-grade spots. We are trying to develop more use cases to encourage us to evolve business logic.
Places, where TDD doesn’t operate well, is with User Interface. It’s frequently tricky to test the UI. If you’re experimenting with the UI and changing it regularly, you’d better avoid TDD because it won’t work in this case.
You use TDD when you have a clear vision about what you want to develop. So the flow is that you’ll first document the concept, then write a test to validate, and the last step is to write the implementation. You have to develop an intuition for TDD and adopt the right workflow.
If you’re doing on Open Source project, you are not a fan of TDD. If you start with TDD, you might have a problem with skipping the refactoring process. If you fall into that trap, your code won’t be maintainable.
What if your project becomes popular and accepted by thousands of people? They don’t suppose to use unmaintainable code in production, but they use.
But not everything is so black if you break something in the phase of refactoring, you can always go ahead and fix it.
Conclusion: TDD is used by (large) teams when they’re building large business systems such as payment services or telecommunication system.
The End is Near
The whole process of testing is trying to communicate with people who are going to be maintaining these tests what is relevant. Don’t make them start thinking:
I’m going to change the INDEX User route, and I’d better not make any changes to the user’s request object because there’s something important there.
The generally developed mistake which I’m fascinated with is when Management (rarely Software Developers)is creating a mandate that you have to have 100% code coverage! BAD IDEA! Take the Pareto’s Principle into consideration.
It’s really hard to wind up this curve of the value that the code coverage provides. Strive for this goal can produce diminishing returns. The last 10% of code coverage can be really finicky and hard to maintain. You have to plan what cannot be planned, often start hanging coerce code to expose certain logic which have to be useful for test.
All that effort so Istanbul can show you 100% test coverage! It’s unnecessary.