TDD in existing function (Django)

Asked

Viewed 403 times

3

I need to learn more about TDD.

What a test it should be to pass?

The function already works, just wanted to know what would be a test?

@login_required
def create_contract(request, proposal_id):
    if request.user.is_authenticated:
        proposal = Proposal.objects.get(pk=proposal_id)

        if proposal.status != 'co':
            return HttpResponse('O status do orçamento deve ser concluido.')
        else:
            contractor = proposal.work.
customer
            contract = Contract(
                proposal=proposal,
                contractor=contractor
            )
            contract.save()
            proposal.status = 'a'
            proposal.save()
    return redirect('/contract/%d' % contract.id)

2 answers

7

TDD is not unit test

TDD is a software development methodology or often functions as a set of principles where testing plays a key role.

What you’re looking for is unit testing. Of course this is all about TDD, but don’t confuse the two.

I will not go into detail as I have written about it, as you can read in some of my other answers:

Thinking of the scenarios

Scenarios of a test are the different situations you want to Tester. In general includes:

  • Success scenarios: those whose routine works as expected.
  • Error/failure/failure/exceptional scenarios: those where you simulate an error situation to test how robust your code is.

Note some people differentiate exceptional scenarios from error scenarios, where the first would be an expected error and the second an unexpected error.

Another differentiation that makes sense in some contexts is to differentiate the main scenario from the alternative scenarios.

For example, in the case of the routine cited in the question, analyzing the conditions of the code, at first I see three scenarios:

  1. Authenticated and proposed user with status co (success)
  2. Authenticated and proposed user with status other than co (exceptional)
  3. User not authenticated (exceptional)

Thinking a little more about error scenarios, what would happen if some method called objects whose routine depends returned unexpected values? Let’s see:

  • Proposal does not exist and Proposal does not contain a real proposal. What happens?
  • An error occurs in some method save (failed to access the database, for example). What happens?

It does not seem to me the case of this routine, but if you are implementing an API, that is, a code that will be used by third parties, it is always good to also test scenarios where the parameters are null or different from expected (or at least to document the expected behavior in these cases).

Creating the environment for a unit test

The most important in a unitary test is that it be unitary in truth. The ideal is that routine does not depend on the functioning of external mechanisms

For example, you will not want the test depending on having a proposal registered in the database and then have this database configured each time you will run the routine.

Of course sometimes we want to run an integration test that tests several objects acting together and even accessing the database, but let’s ignore this approach for now.

One problem you may face in this code is direct or static access to external objects. In some cases this is easily manageable as it is an access planned by the platform, in others it can be make the code very difficult to test. In the case of this routine, you can simply populate your repository with data like in first example of the framework testing documentation.

Another problem of this method is that it does a lot. In addition to applying navigation logic that one controller should do, it also does the creation of objects and manipulation of various entities in the bank. I know it sounds exaggerated, but a slight change of mindset can make you realize that this division makes sense and would make code less coupled and easier to test.

Example:

@login_required
def create_contract(request, proposal_id):
    if request.user.is_authenticated:
        if my_model.is_proposal_budget_completed(proposal_id):
            return HttpResponse('O status do orçamento deve ser concluido.')
        else:
            my_model.create_contract_using_proposal(proposal_id)
    return redirect('/contract/%d' % contract.id)

Note how it is now easier to understand what the method does and the logic is abstracted in a model, can be reused and also replaced using mocks for testing.

A mock is a substitute object that we put in place of the true. It simulates the true object in our test, returning predefined values so that its behavior does not interfere with the test result.

Implementing the tests

To perform a unit test in the routine, just manually construct the necessary parameters for each scenario you want to test, call the routine with these parameters and finally make an assertion to ensure that the result is as expected.

For example, in the success scenario, you can check whether the return of the method, which is a HttpResponseRedirect returned by redirect has the expected values.

Another possibility (although I don’t know how to do this in Python, is to check if the methods that should be called were really called and with the correct values.

In an exceptional scenario with status != 'co', just check the returned message on HttpResponse.

Remember to always consult the documentation the language and framework you are using, as they always provide one or more methods to test in an integrated way.

  • all this is still very new to me, have you give me some examples?

3

Tdd works with a cycle: test fail >> test pass >> refactory >> test fail >> test pass >> test refactory

It’s very complicated to give an exact example because you’re the one who has to know that. But. Each test has a scenario, that is, you want something to happen, you test to see if this something happens and it causes a failure. Starting from that failure, you program and pass the test.

The concept of "refactory", is when you see that your code is not good and can be improved, and with this you repeat the cycle: ... >> Refactory (code improvement) >> test fail >> test pass >> Refactory.

This involves a little common sense and you judge whether your own code is good for you refactoring.

In the case of your view, I identify some scenarios:

1 - If the status is different from co, display Httpresponse; 2 - If the status is different from 'co', save the Tract; 3 - Check if Propoposal changed the status code after saving everything.

"Down," I see these three initial tests.

To work with tests, you can follow this path:

1 - Installing dependencies:

django-nose==1.4 - Ajudará a executar seus tests
model-mommy==1.2.5.1 - Ajudará a criar objetos para seus tests

2 - Create a settings_tests.py in the same Settings directory as your project to run your tests. It will serve for you to create objects in a test database that will then be destroyed. Example:

    # coding: utf-8
    import logging
    from .settings import *

    logging.disable(logging.INFO)

    DEBUG = True

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME':':memory:',
        }
    }
    TEMPLATE_DIRS = (
       os.path.join(BASE_DIR, 'templates'),
    )

    TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
    EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
    BROKER_BACKEND = 'memory'
    SOUTH_TESTS_MIGRATE = False
    SKIP_SLOW_TESTS = True
    RUN_SLOW_TESTS = False

3 - First test By default, each app already comes with the tests.py file, in which you can put your tests there. Everything running right, you must execute the following command:

python manage.py test --settings=seu_projeto.settings_tests

All being right, you will see the message that there are no tests. Creating a basic test class, you have your setup, that is, your test will always be there initially to run your tests. It is always important to observe what your view/class expects to receive for you to send to it before calling it. In your case, your view expects the "proposal_id", and so you must submit this value.

    class MinhaClasseTest(TestCase):

        def setUp(self):
            #isso criará um objeto temporário para o seu test
            self.proposal = mommy.make(Proposal) # from model_mommy import mommy e from .models import Proposal

        def test_status_is_not_co(self):
            self.proposal.status = 'co'
            self.proposal.save()
            resposta = self.client.get('/url-que-chama-essa-view/'+self.proposal.id)
            esperado = 'O status do orçamento deve ser concluido.'
            self.assertEqual(resposta, esperado)

It is very important to note that the ideal test will be told by you, the ideal scenario will be told by you. You are the one who knows what you expect from the call of a method or class, and that is what you expect from your test.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.