Tutorial on Implementing unit tests in Python's flask framework

Source: Internet
Author: User
Tags openid
Overview

In the previous chapters, we focused on adding functionality to our small application step. So far we have an application with a database, you can register users, log users log out of logs and review 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 us prevent failures and rollbacks in future programs.

Let's find the bug.

At the end of the previous chapter, I intentionally introduced a bug in the application. Let me then describe what kind of bug it is, and then look at how it affects our program when it does not follow our wishes.

The problem with the application is that there is no guarantee that the user's nickname is unique. User nicknames are automatically initialized by the application. We will first consider using the nickname of the user given by OpenID provider, and then consider using the user name portion of the email message as the user's nickname. However, if duplicate nicknames occur, subsequent users will not be able to register successfully. Even worse, in modifying user-configured forms, we allow users to arbitrarily change their nicknames, but we still do not check for nickname collisions.

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

Flask Debugging functions

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

Let's create a brand new database, under Linux, to execute:

RM app.db./db_create.py

Under Windows, do the following:

Del App.dbflask/scripts/python db_create.py

We need two OpenID accounts to reproduce this bug. Of course, the ideal state for these two accounts is to come from a different owner, which prevents their cookies from making the situation more complicated. Create a conflicting nickname by following these steps:

    • Log in with your 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 for the second account and change the nickname to "DUP"


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

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

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

This error message is quite clear, we tried to insert a duplicate nickname in the data, the nickname field of the database is a sweatshirt key, so this operation is not valid.

In addition to the actual error, there is a minor error in our hand. If a user does not notice that an error has been raised in our application (an error or any other cause), the application will leak the error message and stack information to him/her instead of exposing it to 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 have been running our application in debug mode, and we have enabled the debug mode of the application by setting Debug=true parameters. Here we configure in the run script run.py.

It is convenient to develop applications like this, but we need to turn off debug mode on the production environment. Let's create another startup script file setting off Dubug mode (filerunp.py):

#!flask/bin/pythonfrom app Import appapp.run (debug = False)

Now restart the app:

./runp.py

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

This time we did not get an error message, instead, we got an HTTP 500 error code, which is an internal server error. While this 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 generates 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 fails, 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 HTTP Error handlers

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

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

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

This place is needless to say, because they are all self-evident. The only interesting place when error 500 is handled in the Rollack statement, this place is indispensable because this method is called 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 it back to prevent a session from turning to a 500 error template.

This is a 404 error in the stencil

 
  {% extends "base.html"%} {% block content%}

File not Found

Back

{% Endblock%}

This is a 500 bug template

 
  {% extends "base.html"%} {% block content%}

An unexpected error had occurred

The administrator has been notified. Sorry for the inconvenience!

Back

{% Endblock%}

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 deal with the second problem we need to configure the app's error reporting mechanism.

The first one is to send us the error log by email whenever an error occurs.

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

# mail server settingsmail_server = ' localhost ' mail_port = 25mail_username = Nonemail_password = None # Administrator List ADMINS = [' you@example.com ']

Of course, it makes sense for you to change the above configuration to your own.

Flask uses a generic Python logging module, so setting up sending error log messages is straightforward. (fileapp/__init__.py):

From config import basedir, ADMINS, Mail_server, Mail_port, Mail_username, Mail_password if not app.debug:  import Logg ing 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 that we is only enabling the emails when we run without debugging.
Note that we want to open the mail feature in non-dubug mode.

It's also easy to test Mail on a PC without a mail server, fortunately Python has an SMTP test-out 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 app receives and sends messages that appear in the console window.

Print Log to File

Receiving the error log via mail is very good, but that's not enough. Some conditions that cause a failure do not trigger an exception and are not the primary issue, so we need to save the log to a log file, and in some cases a log is required for troubleshooting.

For this reason, our application requires a log file.

Open file logs and mail logs are similar (fileapp/__init__.py):

If not app.debug:  import logging from  logging.handlers import rotatingfilehandler  File_handler = Rotatingfilehandler (' Tmp/microblog.log ', ' a ', 1 * 1024x768 * 1024x768, Ten)  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 under the TMP directory, and the file name is Microblog.log. The Rotatingfilehandler method we use has a parameter that limits the number of logs. In this case, we limit the size of a log file to 1M and take the last 10 files as a backup.
Logging. The formatter class provides a definition format for 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, the line number of the information.


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

When we're not using logs, it's very difficult to debug an online and in-use Web service, and writing log information to a file is a useful tool for diagnosing and solving problems, so let's get ready to use this feature now.

Bug fixes

Let's fix the bug that the nickname repeats.

As discussed earlier, there are two places that have not yet dealt with duplication. The first is Flask-login's after_login processing, which is called after the user has successfully logged into the system, and we need to create a new user instance. This is affected by a code fragment 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). First () = = None:      return nickname    Version = 2 while    True:      new_nickname = nickname + str (version)      if User.query.filter_by (nickname = New_nickname). First () = = None: Break      Version + = 1    return new_nickname  # ...

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

The second place that causes duplicate nicknames is to edit the page view function, which is a small prank for the user to choose a nickname, and the correct way is to not allow the user to enter a duplicate name and change the user to another name. We solve this problem by adding form validation, and if the user enters an invalid nickname, they will get a field validation failure message, and add our validation by simply rewriting 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 = +)])   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). First ()    if User! = None:      Self.nickname.errors.append (' This nickname was already in use. Please choose another one. ')      Return False    return True

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

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

@app. Route ('/edit ', methods = [' GET ', ' POST ']) @login_requireddef 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:


Your Nickname:          {{form.nickname (size =)}}      {% for error in form.errors.nickname%}      
[{{error}}] {% ENDFOR%}

Now this bug has been fixed, preventing duplication of data ... Unless these validation methods do not work correctly. This still has a potential problem when two or more threads/processes are accessing the database in parallel, but these are the topics we discuss later in this article.

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

Unit Test Framework

Let's discuss the topic of automated testing by putting the above session on the test first.

As applications grow in size, it becomes increasingly difficult to determine whether changes to the code affect existing functionality.

The traditional method of preventing regression is a good way for you to test all the different functions of an application by writing unit tests, each of which focuses on a single point to verify that the results are consistent with the expectations. The test program verifies that the application is working correctly through regular execution. As test coverage becomes larger, you can confidently modify and add new features, 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/pythonimport unittest from config import basedirfrom app import app, Dbfrom app.models import User class TESTC ASE (UnitTest. TestCase): def setUp (self): app.config[' testing ') = True app.config[' csrf_enabled '] = False app.config[' Sqlalche My_database_uri '] = ' sqlite:///' + os.path.join (basedir, ' test.db ') Self.app = App.test_client () Db.create_all () d  EF TearDown (self): Db.session.remove () Db.drop_all () def test_avatar (self): U = User (nickname = ' john ', email = ' john@example.com ') avatar = U.avatar (+) expected = ' Http://www.gravatar.com/avatar/d4c74594d841139328695756648b6 Bd6 ' assert Avatar[0:len (expected)] = = Expected def test_make_unique_nickname (self): U = User (nickname = ' John ', E    mail = ' john@example.com ') db.session.add (U) db.session.commit () nickname = User.make_unique_nickname (' John ') Assert nickname! = ' john ' U = User (nickname = nickname, email = ' susan@example.com ') db.session.add (u) db.session  . Commit ()  nickname2 = User.make_unique_nickname (' John ') assert nickname2! = ' John ' assert 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, they are executed before and after each test method, and the settings for complex points can contain several sets of tests, each representing a unit test, 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 the different database as the primary database, in the teardown only a simple set of database content can be.

Testing is implemented as a method, and a test should run some application methods of known results, and it should be able to assert the difference between the results and expectations.


So far, there have been two Tests in our test framework. The first verifies that the Gravatar avatar URLs from the previous article were generated correctly, noting that the expected Avatar was hardcoded 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, it creates a new user and writes to the database, and also determines the uniqueness of the name. Next create a second user, it is recommended to use a unique name, you can try to use the first user name. In the second part of the test, the expected result is that the recommended use differs from the previous name.


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

./tests.py

If an error message appears, you will get a report in the console.
Conclusion

Today's discussion of debugging, errors and testing ends here, and I hope this article will be useful to you.

The usual, if you have any comments please write below.

The code of the Weibo app is updated today and you can download it here:


Download 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.