A tutorial on the implementation of unit tests in the Python flask framework _python

Source: Internet
Author: User
Tags assert commit extend openid

Profile

In the previous chapters, we focused on adding functionality to our small application step-by-step. So far we have an application with a database that registers users, logs users to log out of logs, and views modifications to the configuration file.

In this section, we do not add any new functionality to the application, instead we are looking for a way to increase the stability of our written code, and we will also create a test framework to help prevent failures and rollbacks that occur in future programs.

Let's find a bug.

At the end of the previous chapter, I intentionally introduced a bug in my application. Let me now describe what kind of bug it is, and then see how it affects it when our program does not execute as we wish.

The problem with the application is that there is no guarantee of the uniqueness of the user's nickname. User nicknames are automatically initialized by the application. We will first consider using the nickname of the user given by the OpenID provider, and then consider using the user name part of the email message as the user's nickname. However, if a duplicate nickname appears, subsequent users will not be able to register successfully. What's worse, we allow users to arbitrarily change their nicknames in a user-configured form, but we still don't check the nickname conflicts.

When we analyze the behavior of the application after the error is generated, we will locate these problems.

Flask Debugging Features

So let's see what happens when a bug is triggered.

Let's start by creating a brand-new database, under Linux, to execute:

RM app.db
./db_create.py

Under Windows, perform:

Del app.db
Flask/scripts/python db_create.py

We need two OpenID accounts to reproduce the bug. Of course, the ideal state for both accounts is to come from a different owner, so that they can avoid making the situation more complicated by their cookies. Create a conflicting nickname by following these steps:
    • Log in with the first account
    • Go to the User Information properties edit page and change the nickname to "DUP"
    • Log Out System
    • Log in with a second account
    • Modify the user information attribute of the second account to change the nickname to "DUP"


Oh! An exception was thrown in the SQLAlchemy to see the error message:

Lalchemy.exc.IntegrityError
integrityerror: (integrityerror) column nickname is not unique U ' UPDATE user SET Nickname=, about_me=? WHERE user.id =? ' (U ' dup ', U ', 2)

After the error is the stack of this error, in fact, this is a pretty good error hint, you can turn to any framework to check the code or in the browser to execute the correct expression.

This error message is quite clear, and we are trying to insert a duplicate nickname in the data database where the nickname field is a Guardian key, so the operation is invalid.

In addition to the actual error, we have a minor error on hand. If a user does not notice an error in our application (this error or any other cause of the exception), the application will leak the error message and stack information to him/her instead of exposing us. This is a good feature for our developers, but many times we don't want users to see this information.

For so long, we've been running our application in debug mode, and we've set the Debug=true parameters to enable the application's debug mode. Here we configure it in the Run script run.py.

While it is convenient for us to develop applications like this, we need to turn off debug mode on a production environment. Let's create another boot script file setting to turn off Dubug mode (filerunp.py):

#!flask/bin/python from
app import app
app.run (debug = False)

Now restart the application:

./runp.py

And now try renaming the second account nickname into ' DUP '

This time we didn't get an error message and instead we got an HTTP 500 error code, which is an internal server error. Although it is not easy to locate errors, at least no details of our application are exposed to strangers. When an exception occurs after debugging is turned off, Flask produces a 500 page.

Although this is better, there are still two problems. First beautify the problem: the default 500 page is ugly. The second problem is more important, when the user operation failed, we can not get the error message, because the error in the background silently processing. Fortunately there is a simple way to deal with these two problems.

Customizing an HTTP Error handler

Flask provides a mechanism for applications to install their own error pages, as an example, let's define two of the most common HTTP 404 and 500 error custom pages. Customizing other error pages is the same way.

Use a cosmetic to declare a custom error handler (fileapp/views.py):

@app. ErrorHandler (404)
def internal_error (Error): Return
  render_template (' 404.html '), 404
 
@ App.errorhandler (+)
def internal_error (Error):
  db.session.rollback () return
  render_template (' 500. HTML '), 500

This place needs no more words, because they are self-evident. The only interesting place when error 500 deals with the Rollack statement, this place is indispensable because this method will be invoked as an exception. If an exception is caused by a database error, then the session of the database becomes an invalid state, so we need to roll back it to prevent a session from turning to a 500-wrong template.

This is a 404 error in the stencil

<!--extend Base layout-->
{% extends ' base.html '%}
 
{% block content%}
 
 

This is a 500-bug template.

<!--extend Base layout-->
{% extends "base.html"%}
 
{% block content%}
 
 

Note that we will continue to use our base.html layout so that our error page looks more comfortable

Send error logs via email

To address the second issue we need to configure the application's error reporting mechanism.

The first is to send us the error log by email whenever there is a mistake.

First, we need to configure the mail server and admin list in our application (fileconfig.py):

# Mail server settings
mail_server = ' localhost '
mail_port =
mail_username = None
Mail_password = None
 
# Administrator list
ADMINS = [' you@example.com ']

Of course, you have to change the above configuration to your own to make sense

Flask uses the generic Python logging module, so it is easy to set up sending error log messages. (fileapp/__init__.py):

From config import basedir, ADMINS, Mail_server, Mail_port, Mail_username, Mail_password
 
if not app.debug:
  Import logging from
  logging.handlers import smtphandler
  credentials = None
  if Mail_username or Mail_ PASSWORD:
    credentials = (Mail_username, mail_password)
  Mail_handler = Smtphandler (Mail_server, mail_port), ' no-reply@ ' + mail_server, ADMINS, ' microblog failure ', credentials)
  mail_handler.setlevel (logging. ERROR)
  App.logger.addHandler (Mail_handler)

Note so we are only enabling the emails when we run without debugging.
Note that we want to open the mail function in the Dubug mode.

It's also easy to test mail on PCs that don't have a mail server, but luckily Python has an SMTP test-queued Server (SMTP debugging Server). Open a console window and run the following command:

Python-m Smtpd-n-C debuggingserver localhost:25

When the program is running, the application receives and sends messages that are displayed in the console window.

Print Log to File

Receiving error logs via mail is great, but that's not enough. Some of the conditions that cause the failure do not trigger an exception and are not a major problem, so we need to save the log to the log file, and in some cases, log for error.

For this reason, our application requires a log file.

Open file log and mail log very similar (fileapp/__init__.py):

If not app.debug:
  import logging from
  logging.handlers import rotatingfilehandler
  File_handler = Rotatingfilehandler (' Tmp/microblog.log ', ' a ', 1 * 1024 * 1024,)
  file_handler.setformatter (logging. Formatter ('% (asctime) s% (levelname) s:% (message) s [in% (pathname) s:% (Lineno) d])
  App.logger.setLevel (logging). INFO)
  file_handler.setlevel (logging.info)
  App.logger.addHandler (File_handler)
  app.logger.info (' Microblog startup ')


The log file is generated in the TMP directory, and the file name is Microblog.log. The Rotatingfilehandler method we use has a parameter that restricts the number of logs. In this case, we limit a log file size of 1M and the last 10 files as backups.
Logging. The formatter class provides a format for the definition of log information, because it will be written to a file, we want to get as much information as possible, so in addition to log information and stack information, we also write a timestamp, log level and file name, information line number.


To make the log more useful, we lowered the log level of the application log and the file log handler because we would have the opportunity to write useful information to the log without error. As an example, we set the log level to the information level at startup. From now on, every time you start your application, you will record your debugging information.

When we are not using the log, it is very difficult to debug a Web service online and in use, write log information to the file, will be a useful tool for us to diagnose and solve the problem, so let's get ready to use this feature now.

Bug fixes

Let's fix the bug where the nickname repeats.

As discussed earlier, there are two places where duplication is not currently addressed. The first is Flask-login's after_login processing, which will be invoked after the user successfully logs on to the system, and we need to create a new user instance. This is affected by a snippet of code that we did fix (fileapp/views.py):

If user is none:
    nickname = Resp.nickname
    if nickname is None or nickname = = "":
      nickname = Resp.email.split ('  @ ') [0]
    nickname = User.make_unique_nickname (nickname)
    user = User (nickname = nickname, email = resp.email, role = Role_user)
    Db.session.add (USER)
    db.session.commit ()


The way we solve this problem is to have the user class choose a unique name for us, which is what the Make_unique_nickname method does (fileapp/models.py):

Class User (db. Model):
  #
  ... @staticmethod
  def make_unique_nickname (nickname):
    if User.query.filter_by (nickname = nickname). None: Return
      nickname
    Version = 2 while
    True:
      new_nickname = nickname + str (version)
      if User.query.filter_by (nickname = New_nickname).-A () = None: Break
      version + + 1 return
    new_nickname< c22/># ...

This method simply adds a counter to generate a unique nickname name. For example, if the username "Miguel" exists, this method will suggest that you use "Miguel2", but if it exists it will generate "Miguel3" ... Note that this method is set to a static method because it does not apply to instances of any class.

The second place to create a duplicate nickname is to edit the page view function, which is a small prank for the user to choose a nickname, and the correct way is not to allow the user to enter a duplicate name and replace it with another name. We solve this problem by adding form form validation, and if the user enters an invalid nickname, it will get a field validation failure message, adding our validation simply rewrite the form's Validate method (fileapp/forms.py):

 
Class EditForm (Form):
  nickname = TextField (' nickname ', validators = [Required ()])
  About_me = Textareafield (' About_me ', validators = [Length (min = 0, max = 140)])
 
  def __init__ (self, original_nickname, *args, **kwargs):
    Form . __init__ (self, *args, **kwargs)
    self.original_nickname = Original_nickname
 
  def validate (self):
    if not Form.validate (self): return
      False
    if self.nickname.data = = Self.original_nickname: return
      True
    user = User.query.filter_by (nickname = Self.nickname.data). (a)
    if user!= None:
      Self.nickname.errors.append (' This nickname be already in use. Please choose another one. ')
      Return False to
    True

The form's constructor adds a new parameter original_nickname, the validation method validate use this parameter to determine whether the nickname has been modified, if it has not been modified to return it directly, if it has been modified, the method will confirm whether the new nickname is already present in the database.

Next we add a new constructor parameter to the view function:

@app. Route ('/edit ', methods = [' Get ', ' POST '])
@login_required
def edit ():
  form = EditForm ( g.user.nickname)
  # ...

To complete this modification we must also enable the Error display field (file app/templates/edit.html) in the form's template:


<td>your nickname:</td>
    <td>
      {{form.nickname (size =)}}
      {% for error in Form.errors.nickname%}
      <br><span style= "color:red;" >[{{error}}]</span>
      {% endfor%}
    </td>

Now that the bug has been fixed, it's blocking the appearance of duplicate data ... Unless these validation methods are not working properly. There is still a potential problem with two or more threads/processes accessing the database in parallel, but these are the topics discussed later in our article.

Here you can try to choose a duplicate name to see how the form handles these errors.

Unit Test Framework

Let's talk about the test session first and let's discuss the topic of automated testing.

As the size of the application grows, it becomes increasingly difficult to determine whether changes in the code will affect existing functionality.

The traditional method of preventing regression is a good way to test your application for all the different functions by writing unit tests, each focusing on a single point to verify that the results are consistent with what you expect. The test program confirms that the application is working properly through periodic execution. When the test coverage becomes larger, you can confidently modify and add new functionality, simply by testing the program to verify that the existing functionality of the application is affected.


Now we use the Python unittest test component to create a simple test framework (tests.py):

#!flask/bin/python import unittest from config import basedir to app Import app, db from app.models import User CLA SS TestCase (UnitTest. TestCase): def setUp (self): app.config[' testing '] = True app.config[' csrf_enabled '] = False app.config[' Sqla Lchemy_database_uri '] = ' sqlite:///' + os.path.join (basedir, ' test.db ') Self.app = App.test_client () db.create_all () def teardown (self): Db.session.remove () Db.drop_all () def test_avatar (self): U = User (nickname = ' J Ohn ', email = ' john@example.com ') avatar = U.avatar (128) expected = ' http://www.gravatar.com/avatar/d4c74594d84113 9328695756648b6bd6 ' assert Avatar[0:len (expected)] = = Expected def test_make_unique_nickname (self): U = User (n Ickname = ' john ', email = ' john@example.com ') db.session.add (U) db.session.commit () nickname = User.make_uniqu E_nickname (' John ') assert nickname!= ' john ' U = User (nickname = nickname, email = ' susan@example.com ') db.ses SioN.add (U) db.session.commit () nickname2 = User.make_unique_nickname (' John ') assert nickname2!= ' John ' Asse
 RT nickname2!= Nickname If __name__ = = ' __main__ ': Unittest.main ()

The discussion of the UnitTest test component is beyond the scope of this article, and we just need to know that TestCase is our test class. The setup and Teardown methods are special in that they are executed before and after each test method, and the settings for the complex point can contain several sets of tests, each representing a unit test, and TestCase subclasses and each group will have separate setup and Teardown methods.


These special setup and Teardown methods are very common, in the setup can easily modify the configuration, for example, we want to test different databases as the main database, in the teardown inside simply set the database content can be.

The test is implemented as a method, a test should run some of the known results of an application method, and should be able to assert the difference between the results and the expected.


So far, we have two Tests in our test framework. The first validation comes from the correctness of the Gravatar avatar URLs generated from the previous article, noting that the expected avatar is hard-coded in the test and compared to the objects returned in the user class.

The second Test validation is the Test_make_unique_nickname method, which is also in the user class. This test is a bit verbose, creating a new user and writing to the database, while determining the uniqueness of the name. Next to create a second user, we recommend using a unique name, you can try to use the first user name. In the second part of the test the expected result is recommended to use a different name than before.


To run this test suite you just run the tests.py script:

./tests.py

If an error message occurs, you will receive a report at the console.
Conclusion

Today's discussion of debugging, errors and testing ends here, and I hope this article works for you.

As usual, if you have any comments please write it down below.

The microblogging application code today modifies the update that you can download here:

The


Downloads the microblog-0.7.zip.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.