Database Test-First, Is That Even Exist?


Hello guys, it's me again and again, Wicaksono as a backend engineer from Allocateam. This time I want to share you about our method to test-first our database in development. TDD is one of our project requirement, so we must apply TDD in every aspect of our project, and it's kinda frustrating for me as one of my tasks is to implement the database models. There are a lot of pros and cons when searching for the idea of implementing the TDD for database development, there are more cons written on the internet though! 

Most people said that the idea of TDD is great, but implementing it in the database is hard, impractical, and doesn't worth it (comparing the benefits and the efforts). According to Greg Lucas who has been working on SQL and database related works for more than 14 years, he said that, yes it is hard, and even in most agile teams, TDD for databases, if implemented at all, is often the last thing to be put into action. Nevertheless, he also pointed some of the advantages of implementing TDD and why we should bother. You can read more about his writing here.

Thanks to his writing, now it's not about is it possible or not. It's now about how can we implement it on our project. Following his idea in his next article, I kinda figure it out how.

On the previous article, I only use built-in python unittest for testing, but now I think I need an extension called flask-testing to help me. Install it using pip with:

1
pip install Flask-Testing

So here is the example case, I want to create a profile model that contains the name (required) and a phone number. Quite simple right? Well, you can watch our repo for our real implementation of the database models TDD in a more complex structure.

Our project uses SQLAlchemy as our ORM and we placed our model on a file called models.py. Now let's make our model stub first:

1
2
3
4
5
6
7
8
from flask_sqlalchemy import SQLAlchemy


db = SQLAlchemy()


class Profile(db.Model):
    id = db.Column(db.Integer, primary_key=True)

This is how I create our stub for our models, passing it none will cause unreadable error telling the model is broken, so I put a primary key as it's a must-have attribute for a model class. Remember, this is only a stub and we haven't implement anything for the profile model.

Now let's create our test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from flask import Flask
from flask_testing import TestCase
from models import db
from sqlalchemy import exc

from models import (
    Profile
)

class TestDatabase(TestCase):

    def create_app(self):
        app = Flask(__name__)
        app.config['TESTING'] = True
        app.config.from_object('config.TestingConfig')
        app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
        db.init_app(app)
        return app

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def test_profile_ok(self):
        profile = Profile()
        profile.name = 'Wicaksono'
     profile.phone = '08111710107'
        db.session.add(profile)
        db.session.commit()

        self.assertIn(profile, db.session)

    def test_profile_no_name(self):
        profile = Profile()
     profile.phone = '08111710107'
        db.session.add(profile)

        with self.assertRaises(exc.IntegrityError):
            db.session.commit()

    def test_profile_no_phone(self):
        profile = Profile()
        profile.name = 'Wicaksono'
        db.session.add(profile)
        db.session.commit()

        self.assertIn(profile, db.session)

Here is the explanation:

create_app: is to create an app with a custom configuration. Sometimes we need to define a different configuration for our testing environment, here, for example, I define a different testing database address so it doesn't create a conflict with my local development database.

  • setUp: is to tell the test how we initiate our app, here I tell the test to create the database schema according to the model.
  • tearDown: is to tell the test what should it do after it done testing, here I tell the test to remove the session and destroy all of the database schemas inside the test database.
  • test_profile_ok: is to test if we passed all of the attributes for the profile model, the model should be successfully committed.
  • test_profile_no_name: is to test if we didn't pass a name for the profile model, the model should raise IntegrityError at commit.
  • test_profile_no_phone: is to test if we didn't pass a phone number for the profile model, the model should be successfully committed because the phone number is not a must.
When we run the test we should expect all of those tests would fail, if it's, that our [RED]. Now let's implement our model:

1
2
3
4
5
6
7
db = SQLAlchemy()


class Profile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(15), nullable=False)
    phone = db.Column(db.String(15))

If our tests have passed after we run it when we have implemented the model, that means our model have met our criteria. That's our [GREEN], well done!

It's quite confusing and time taking for me and maybe you guys to reverse our thinking to apply test-first for more complex model structure, but it just needs some practice! On every next model that I have to implement with TDD, it getting faster and easier to do, so keep it up! Hope my experience will help some of you guys, see you next time!

0 komentar:

Post a Comment