annotate mrjunejune/src/blog/thoughts-on-tdd/index.md @ 169:295ac2e5ec00

[MrJuneJune] Created separate target for generating html from md.
author MrJuneJune <me@mrjunejune.com>
date Mon, 19 Jan 2026 17:33:18 -0800
parents 1c0878eb17de
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
169
295ac2e5ec00 [MrJuneJune] Created separate target for generating html from md.
MrJuneJune <me@mrjunejune.com>
parents: 158
diff changeset
1 ---
295ac2e5ec00 [MrJuneJune] Created separate target for generating html from md.
MrJuneJune <me@mrjunejune.com>
parents: 158
diff changeset
2 title: Thoughts on TDD
295ac2e5ec00 [MrJuneJune] Created separate target for generating html from md.
MrJuneJune <me@mrjunejune.com>
parents: 158
diff changeset
3 description: Examining Test-Driven Development practices, unit testing debates, and the challenges of implementing TDD effectively in real-world projects.
295ac2e5ec00 [MrJuneJune] Created separate target for generating html from md.
MrJuneJune <me@mrjunejune.com>
parents: 158
diff changeset
4 ---
295ac2e5ec00 [MrJuneJune] Created separate target for generating html from md.
MrJuneJune <me@mrjunejune.com>
parents: 158
diff changeset
5
100
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
6 # Thoughts on TDD
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
7
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
8 Is testing important? Ask yourself that question. If you had to think about it for more than a few seconds, you’re either an inexperienced programmer or someone who has never had to release a product to a large group of users. Testing is not just important—it’s essential. It ensures that the software you release is less buggy and more stable because it allows you to catch issues before your customers do. That’s the "why" behind testing that everyone would agree to.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
9
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
10 The real debates arise around the *how*—specifically, approaches to testing, including methodologies like Test-Driven Development (TDD). Over my career, I’ve worked at multiple companies, all of which practiced TDD in some form. This often involved writing unit tests, integration tests, and end-to-end (e2e) tests. However, despite the commonality of TDD, every company seemed to implement it in its own convoluted way, making the code harder to write, debug, and maintain. I want to talk about this practice and my problem with it.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
11
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
12 ## Unit Tests
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
13
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
14 > "Unit testing is the process where you test the smallest functional unit of code. Software testing helps ensure code quality, and it's an integral part of software development. It's a software development best practice to write software as small, functional units then write a unit test for each code unit."
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
15 >
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
16 > — Definition from AWS
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
17
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
18 You might agree with the above definition, disagree, or be unsure about what qualifies as a "unit," but most people are likely to agree with it overall. Now, imagine you’re a 21-year-old physics graduate with three months of self-taught coding experience (and a serious League of Legends addiction). Somehow, you land your first job as a software engineer and are tasked with writing a serializer for a `GET` API in Django—and... you guessed it! testing it!
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
19
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
20 Here's what such a serializer might look like:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
21
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
22 ```python
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
23 from rest_framework import serializers
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
24
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
25 class FooSerializer(serializers.Serializer):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
26 name = serializers.CharField(max_length=100)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
27 unit_price = serializers.FloatField()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
28 quantity_on_hand = serializers.IntegerField(default=0)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
29
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
30 def create(self, validated_data):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
31 return Foo.objects.create(**validated_data)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
32
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
33 def update(self, instance, validated_data):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
34 instance.name = validated_data.get('name', instance.name)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
35 instance.unit_price = validated_data.get('unit_price', instance.unit_price)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
36 instance.quantity_on_hand = validated_data.get('quantity_on_hand', instance.quantity_on_hand)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
37 instance.save()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
38 return instance
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
39 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
40
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
41 If you’re unfamiliar with Django, the `create` and `update` methods save or update records in the database. It’s normal to serialize an object like this into a response for use in an API endpoint, often with a tool like `JSONRenderer().render(foo.data)`.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
42
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
43 Now, we understand detailed implementation as much as senior INSERT_LIBRARY engineer, the question is: *How do you write a unit test for this?*
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
44
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
45 There are two main reasons why this serializer is hard to test as a true "unit":
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
46
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
47 **Dependency on Framework Classes**
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
48
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
49 The serializer inherits from Django REST Framework’s `Serializer` class, which comes with built-in behaviors for things like validation and field handling. If you test whether `name` exceeds 100 characters or if `unit_price` is a float, you’re essentially testing whether Django itself works, which is redundant. But you still need to create a test given a *correct* vallue and *incorrect* value because you are testing more for business logic rather than the code. So are we creating unit test for the function ? or the library?
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
50
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
51 **Database Dependency**
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
52
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
53 The `create` and `update` methods interact with the database directly. Testing them requires either mocking the database (which introduces complexity). If you try to write a test that checks whether the `create` method works, you might end up mocking the `Foo` model, overriding its methods, and verifying that the mock functions were called with the correct arguments. While this might make sense for complex logic, it often feels like overkill for a simple serializer.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
54
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
55 Here’s an example of how you might write such a *unit* test:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
56
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
57 ```python
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
58 from unittest.mock import patch
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
59 from rest_framework.exceptions import ValidationError
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
60
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
61 CORRECT_DATA = {
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
62 'name': 'Test Item',
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
63 'unit_price': 10.99,
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
64 'quantity_on_hand': 5
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
65 }
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
66 BAD_DATA = {
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
67 'name': 'Test Item' * 100,
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
68 'unit_price': "yo",
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
69 'quantity_on_hand': 5
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
70 }
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
71
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
72 def test_serializer_create():
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
73 serializer = FooSerializer(data=CORRECT_DATA)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
74
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
75 # This is where you are testing the frameworks....
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
76 assert serializer.is_valid()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
77
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
78 with patch('app.models.Foo.objects.create') as mock_create:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
79 serializer.save()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
80 mock_create.assert_called_once_with(
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
81 name='Test Item',
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
82 unit_price=10.99,
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
83 quantity_on_hand=5
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
84 )
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
85
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
86 # And you need to create 3 more! (2 update using CORRECT_DATA, and BAD_DATA and 1 with create only)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
87 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
88
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
89 This test ensures that the `create` method is called with the right data. But if the serializer logic gets more complex, the mocking can become cumbersome and annoying. Think about below case, where `FooSerializers` needs to have a redis singleton because it needs to store certain data inside of redis for faster accesibility for few minutes.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
90
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
91 ```python
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
92 class FooSerializer(serializers.Serializer):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
93 ...
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
94 def create(self, validated_data):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
95 self.bar(**validated_data, { ttl: 1000 })
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
96 ...
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
97
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
98 @cache_property
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
99 def bar(self):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
100 return self.get_bar()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
101
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
102 def get_bar(self):
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
103 return Bar()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
104 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
105
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
106 In this case, the `get_bar` method is introduced to facilitate testing of the singleton behavior. This allows you to override the `get_bar` method during tests to avoid interacting with the actual Redis singleton. However, this adds another layer of complexity to your tests.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
107
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
108 ```python
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
109 def test_serializer_create():
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
110 serializer = FooSerializer(data=CORRECT_DATA)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
111
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
112 assert serializer.is_valid()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
113
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
114 # Mocking the database interaction
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
115 with patch('app.models.Foo.objects.create') as mock_create:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
116 # Mocking the singleton method
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
117 with patch.object(serializer, 'get_bar', return_value=MockBar()) as mock_bar:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
118 serializer.save()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
119 mock_bar.assert_called_once_with(
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
120 name='Test Item',
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
121 unit_price=10.99,
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
122 quantity_on_hand=5
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
123
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
124 )
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
125 mock_create.assert_called_once_with(
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
126 name='Test Item',
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
127 unit_price=10.99,
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
128 quantity_on_hand=5
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
129 )
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
130 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
131
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
132 You can imagine how the unit test would look as more cascading effects are introduced. With all these 76 lines of code (19 for each case, accounting for 4 tests: 2 with correct data values and 2 with incorrect data values), it essentially tests...
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
133
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
134 1. Whether the `create`, `update`, and `Redis` functions are called.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
135 2. Whether the data has the correct units.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
136
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
137 <div class="center"> <img src="https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExbHpyY3A5ZjlzNzF5Nmp2Zm13M3kzZWU4Znl4djNmZWoxemRkaHVhbSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/5sNxGQsE3RXap07jQ4/giphy.webp" /> </div>
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
138
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
139
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
140 ### Different Approach: Unit Testing in Rails
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
141
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
142 In frameworks like Ruby on Rails, a different paradigm is used for handling tests, one that might directly conflict with the traditional definition of unit tests. Instead of relying heavily on mocking and isolating functions, frameworks like Rails encourage spinning up a test database or any external dependencies that are *essential*. Here’s how the same serializer would look in Rails using Active Record:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
143
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
144 ```ruby
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
145 class Foo < ApplicationRecord
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
146 validates :name, presence: true, length: { maximum: 100 }
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
147 validates :unit_price, numericality: true
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
148 end
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
149 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
150
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
151 And here’s how a test might look:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
152
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
153 ```ruby
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
154 require 'test_helper'
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
155
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
156 class FooTest < ActiveSupport::TestCase
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
157 test "validates name length and prevents invalid record from saving" do
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
158 foo = Foo.new(name: "a" * 101, unit_price: 10.99, quantity_on_hand: 5)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
159
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
160 assert_not foo.valid?, "Foo should be invalid when name exceeds 100 characters"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
161 assert_equal ["is too long (maximum is 100 characters)"], foo.errors[:name]
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
162 assert_not foo.save, "Foo with an invalid name should not be saved to the database"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
163 assert_nil Foo.find_by(name: "a" * 101), "No Foo record with an invalid name should exist in the database"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
164 end
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
165
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
166 test "saves valid record to the database" do
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
167 foo = Foo.new(name: "Valid Item", unit_price: 10.99, quantity_on_hand: 5)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
168
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
169 assert foo.valid?, "Foo should be valid with correct attributes"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
170 assert foo.save, "Foo with valid attributes should be saved to the database"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
171
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
172 # Look for saved foo and bar
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
173 saved_foo = Foo.find_by(name: "Valid Item")
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
174 saved_bar = Bar.find_by(name: "Valid Item")
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
175
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
176 assert_not_nil saved_foo, "Foo record should exist in the database"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
177 assert_equal 10.99, saved_foo.unit_price, "Foo's unit_price should match the saved value"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
178 assert_equal 5, saved_foo.quantity_on_hand, "Foo's quantity_on_hand should match the saved value"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
179 assert_equal 10.99, saved_bar.unit_price, "Bar's unit_price should match the saved value"
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
180 end
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
181 end
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
182 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
183
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
184 In this example, you’re not mocking anything. Instead, you’re leveraging the test database to directly validate business logic. This approach often feels cleaner and less convoluted than mocking, especially for frameworks with tightly integrated ORM layers like Active Record. If your application also involves a Redis instance or similar components, you would create those as part of your test setup and verify their existence. This approach, however, goes strictly against AWS's definition of unit tests. At my previous job, I remember having a heated argument with an engineer over this—he was a strong believer in AWS's strict definition, while I preferred a looser interpretation based on my experience with Rails.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
185
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
186 **(And no, starting up a Docker container for a Redis instance or database is not SOOO slow that shouldn't be done. This is a widely accepted practice in many engineering companies, including Shopify and Airbnb.)**
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
187
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
188 If you ask me, I’d argue that Rails' approach is slightly better because you avoid the overhead of creating numerous mock objects. For example, in Jest or Python, mocking can get out of hand—especially if you need to mock React hooks that query or mutate global states instead of initializing the state directly within the test. It’s overwhelming to see ten different mock values inserted into a single object, and now imagine having to do that for every single function you write. But now, we will have conflict with...
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
189
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
190 ## Integration Tests
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
191
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
192 > "A type of software test that verifies how different components of your application and services interact and work together as a system, ensuring data flows correctly between them and that the overall functionality is as expected."
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
193 >
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
194 > — Definition from AWS
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
195
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
196 We’ve essentially already done this in our Ruby on Rails testing, so let’s revisit our `FooSerializer`. Integration tests ensure that the serializer works as expected. Here's an example of such a test:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
197
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
198 ```python
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
199 def test_serializer_create():
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
200 serializer = FooSerializer(data=CORRECT_DATA)
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
201
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
202 assert serializer.is_valid()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
203 serializer.save()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
204 assert Foo.objects.filter(name=CORRECT_DATA.name).exists()
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
205 ```
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
206
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
207 At this point, we’re essentially rewriting the same tests but with less mocking. So, what’s the real value of this? There has to be a scenario where unit tests are useful, and integration tests aren’t—or vice versa. In this particular case, though, it’s hard to distinguish that line. As applications grow more complex, these distinctions may become clearer. However, for most scenarios like this one, if tests weren’t written at all, it would be difficult to identify what’s missing or redundant.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
208
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
209 In this specific case, many would agree that writing both unit tests and integration tests offers little value. It’s almost redundant and doesn’t justify the time and effort required.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
210
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
211 ## E2E Tests
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
212
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
213 I couldn't find a definition so I am going to just ask gemini for it.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
214
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
215 > End-to-end (E2E) testing is a software testing method that verifies how a software product works from start to finish. It's also known as system testing or broad stack testing
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
216 >
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
217 > — Gemini version that doesn't create controversial images.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
218
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
219 Finally, let’s talk about end-to-end (E2E) tests. The serializer function we wrote will likely live inside some API endpoint, and I don’t feel like writing codes for it so I hope you guys can imagine it. In most cases, E2E tests are run through a virtual machine (VM) or Docker container that replicates a smaller version of the real server and database, seeded with factory data. These environments are spun up during CI/CD pipelines, where tests simulate real server requests.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
220
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
221 E2E testing is extremely useful when your server is self-contained. However, if your application relies on third-party services, things can get tricky. For example, let’s consider a case where your application depends on an AWS service to check content safety. You can’t easily mimic that service locally, so you’d need to test against their live servers, which may incur costs for every request. As a result, you’ll likely want to separate those tests from your main CI/CD pipeline and use a dedicated staging environment to avoid mixing with production accounts.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
222
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
223 That’s a *good* case. A *bad* case is when the third-party service doesn’t allow any testing at all. For instance, Google’s SSO or similar services might block testing to prevent potential DDoS attacks (Because it is a DDoS attack). In these scenarios, your E2E tests often become glorified integration tests, where you simulate the service instead of interacting with the real thing.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
224
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
225 In short, TDD in its purest sense—using unit, integration, and E2E tests together—is often impractical or unachievable. I haven’t even touched on the time required to run E2E tests, the context-switching needed to write or debug them, flakiness, and other challenges. Still, we write tests because they are critical to delivering reliable software. After all, we’re not open-source engineers releasing products that randomly break because of dependencies like `is_number` in npm, right? Right?
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
226
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
227
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
228 ## My Thoughts
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
229
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
230 We should primarily focus on integration and E2E tests that validate real user activity. It’s not always necessary to test the entire flow, especially when certain parts—like SSO—can only be mocked and not tested fully. E2E tests are often slow, flaky (the worst!), and sometimes impossible to implement comprehensively.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
231
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
232 Unit tests should be reserved for complex functions that require isolation. My general approach to testing is like a binary search:
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
233
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
234 - If a unit test fails, the problem is always within the business logic of that specific function.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
235 - If an integration test fails, the issue is likely due to mismatched input/output between components.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
236 - If an E2E test fails, it could indicate a third-party service outage, a race condition, or a broader system issue.
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
237
65e5a5b89a4e [Seobeo] Migrated everything to this page.
June Park <parkjune1995@gmail.com>
parents:
diff changeset
238 For simple components like our `FooSerializer`, writing three separate tests (unit, integration, and E2E) is overkill at best and a waste of time at worst. Instead, focus on testing critical user flows and isolating tests to specific areas when necessary. This strikes a balance between test coverage and productivity, avoiding the trap of excessive and redundant testing.