Realistic TDD: How I Adapt Test-Driven Development for My Projects

Widyanto H Nugroho
7 min readApr 11, 2022

Test-Driven Development (TDD) had always been the go-to software development process in my university’s courses that involved developing software (e.g., web programming with Django and Microservices with Java Springboot), so the jargon RED-GREEN-REFACTOR is probably ingrained in every one of my friends by now. Anybody in my class can easily and effortlessly explain the philosophy and methodology of TDD.

Quick Review on TDD

Test-Driven Development is an evolutionary software development process developed by Kent Beck in the late ’90s. In essence, you build your software in small pieces, piece-by-piece, with each piece having three phases

  • RED: create the test for your next piece. Of course, running the tests now will report an error (aka red) because you had no implementation for this test.
  • GREEN: code the implementation for the previous test until it passed. Now, running the test will report no errors (green).
  • REFACTOR: refactor the code just implemented to make it well structured.

How Should I Test My Software?

Software developers tend to be very opinionated about testing. Because of this, they have differing opinions about how important testing is and ideas on how to go about doing it. That said, let’s look at three guidelines that (hopefully) most developers will agree with that will help you write valuable tests:

Tests should tell you the expected behavior of the unit under test.

Therefore, it’s advisable to keep them short and to the point. The GIVEN, WHEN, THEN structure can help with this:

  • GIVEN — what are the initial conditions for the test?
  • WHEN — what is occurring that needs to be tested?
  • THEN — what is the expected response?

So you should prepare your environment for testing, execute the behavior, and, in the end, check that the output meets expectations.

Each piece of behavior should be tested once — and only once.

Testing the same behavior more than once does not mean that your software is more likely to work. Tests need to be maintained too. If you make a small change to your codebase and then twenty tests break, how do you know which functionality is broken? When only a single test fails, it’s much easier to find the bug.

Each test must be independent of other tests.

Otherwise, you’ll have a hard time maintaining and running the test suite.

TDD Example in Python Project

When doing TDD, it is a ground rule to test only 1 exact function or part at a time. My python project has Service and Accessor. At a single test, we only test either services or accessors.

Mocking

Mocking is the act of creating pre-programmed objects with expectations which form a specification of the calls they are expected to receive

Martin Fowler

With mocking, we can create an instance of the dependent service, which we can preprogram to behave a certain way on a certain input.

Mocking in Python, we can use the library mock and use object of MagicMock to mock a certain object or package. Here is an example of instantiating unittest in python and mocking certain objects.

class TestService(TestCase):
def setUp(self) -> None:
self.user_accessor = MagicMock()
self.firebase_provider = MagicMock()
self.auth_service = AuthService(
user_accessor=self.user_accessor,
firebase_provider=self.firebase_provider,
)

As you can see, I have AuthService that has UserAccessor and FirebaseProvider and we mock both of them using MagicMock . When using MagicMock , you can return the value of object’s method using return_value attribute. For example, I want to mock return value of user_accessor.get() . When using mock, I can use user_accessor_mock.get.return_value = User() .

Currently, I am working on the authorization feature for my software project using Firebase. One of the methods is to decode the token and get the user credentials firebase_uid.

def authorize(self, token: str) -> Optional[User]:
decoded_token = self.firebase_provider.decode_token(token)
user = self.user_accessor.get_by_firebase_uid(
decoded_token.user_id)
if user:
return user
return self.user_accessor.create_user(
CreateUserSpec(
firebase_uid=decoded_token.user_id,
phone_number=decoded_token.phone_number,
)
)

The code above is my authorization method at the service level. Thus, we mock the user_accessor and firebase_provider . Here is an example of a positive case of the test.

def test_auth_login_succeed(self):
SEED = 1001
token = self._get_token(SEED)
decode_token_return = self._get_firebase_decoded_cred(SEED)
dummy_user = self._get_dummy_user(SEED)
self.mock.get_by_firebase_uid.return_value = dummy_user
self.mock.decode_token.return_value = decode_token_return
user = self.auth_service.login(token) self.assertEqual(user.firebase_uid, dummy_user.firebase_uid)

Remember, we only test the service level without bothering about what happens at the accessor level. So we use mock to do this. Mock will return the value we desire for specific object that already stubbed with MagicMock.

And below is example of negative case.

def test_auth_login_failed_decode_token(self):
SEED = 1002
token = self._get_token(SEED)
decode_token_return = None
self.mock.decode_token.return_value = decode_token_return user = self.auth_service.login(token) self.assertEqual(user, None)

Then, what is the importance of using TDD?

Better design

TDD is in most cases a design process rather than a testing process This is because the written test code tends to be more cohesive and can reduce coupling.

Efficient

Using TDD will identify errors or errors quickly when new code is added to the system. In addition, by implementing TDD, we no longer need to make tests after code implementation is complete.

Test assets

Test cases written automatically in TDD are assets for improving or modifying code so that unit tests can automatically identify the occurrence of new defects/defects in the code.

Reduced flaw injection

Errors are more prone to occur during code maintenance or code changes than during new code implementations.

The advantages and disadvantages of TDD

Some of the advantages that we get when using Test Driven Development are

  1. When an error occurs, we will know exactly where the error is.
  2. Compared to testing the system as a whole, doing TDD provides a fast time in merging a system.

Disadvantages :

  1. It is difficult to determine the correct unit test in a case study because there is no measure that indicates the correctness of a unit test that is built.
  2. The development team found it difficult to abstract from an interface because there was no physical design carried out at the beginning of the application development process. The builder team only builds applications with design thinking.
  3. In building applications with this methodology, a team of builders who are experienced and understand how to write good tests and also understand a little about good architecture is not suitable for novice builder teams.

More on Testing

When testing software, we can separate this into two parts, functional testing, and non-functional testing. Functional testing basically tests whether your software runs correctly or not. While non-functional is more on how good the performance is, is it secure enough? and so on.

In functional testing, sometimes we know the terms black-box testing and white-box testing.

Black-box testing is like, you prepare the input for lots and lots of possible strings, and you check whether the output is correct, even though you don’t know what the inside implementation was like.

Different from the white box testing. In white-box testing you sometimes will check the inside working of the function, are all of them mocked correctly, are the camel case function calling some certain functions, and so on.

Software Quality Assurance

Software Quality Assurance (SQA) is the person responsible for quality assurance planning, faults, record keeping, analysis, and reporting. Software quality assurance is a protected activity that is applied to the entire software process. This process can be carried out by a QA Tester or by a QA Engineer.

The QA Tester has the main task of carrying out tests on devices or emulators, creating test flows, and making test result reports. Meanwhile, QA Engineers are usually tasked with creating automated test programs, making test reports, providing input on the applications being tested, and communicating with interested parties, such as UI/UX developers, back-end, or product managers (PM).

Finale

I cannot imagine building my large projects without TDD. TDD is an amazing framework, but the rigidness made some people scared. By writing a simple test-driven whenever I felt uncertain, I can develop applications seamlessly, while still achieving TDD’s principle:

  • There is always a test for each developed code.
  • Software is built in small incremental pieces, where each piece is well tested.

And that’s about it. Hope by reading this article, you will see past the rigidness and give TDD a try.

Source

--

--