The Python Flask framework provides a unit test tutorial.

Source: Internet
Author: User
Tags openid

The Python Flask framework provides a unit test tutorial.

Summary

In the previous section, we focused on adding features step by step for our small applications. So far, we have an application with a database that can register users, record user login and exit logs, and view and modify configuration files.

In this section, we do not add any new features to the application. Instead, we need to find a way to increase the stability of the code we have written, we will also create a testing framework to help prevent future program failures and rollback.

Let's look for bugs.

At the end of the previous chapter, I intentionally introduced a bug in the application. Next, let me describe what kind of bug it is, and then let's see what kind of impact it has when our program is not executed as we want.

The problem with applications is that the uniqueness of user nicknames is not guaranteed. The user nickname is automatically initialized by the application. First, we will consider using the user nickname given by OpenID provider, and then using the username section in the Email information as the user nickname. However, if a duplicate nickname appears, subsequent users cannot register successfully. What's worse, in the form of modifying user configurations, we allow users to arbitrarily change their nicknames, but we still do not check for nickname conflicts.

After analyzing the behavior of the application when an error occurs, we will locate these problems.

Debugging of Flask

Let's see what happens when a bug is triggered.

Let's create a brand new database. In linux, execute:
 

rm app.db./db_create.py

In Windows, run:
 

del app.dbflask/Scripts/python db_create.py
We need two OpenID accounts to reproduce this bug. Of course, the best status of these two accounts is from a different owner, which can avoid making the situation more complicated by their cookies. Follow these steps to create a conflicting nickname:
  • Log in with the first account
  • Go to the user information attribute editing page and change the nickname to "dup"
  • Logout System
  • Log in with the Second Account
  • Modify the user information attribute of the Second Account and change the nickname to "dup"


Alas! Sqlalchemy throws an exception. Let's take a look at the error message:

lalchemy.exc.IntegrityErrorIntegrityError: (IntegrityError) column nickname is not unique u'UPDATE user SET nickname=?, about_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 message. You can turn to any framework to check the code or execute the correct expression in the browser.

This error message is quite clear. We try to insert a duplicate nickname in the data. The database nickname field is a sweater key, so this operation is invalid.
 

In addition to actual errors, there is a secondary error at hand. If a user does not pay attention to an error in our application (this error or an exception caused by any other reason ), the application will expose error messages and stack information to him/her instead of us. This is a good feature for our developers, but we often don't want users to see this information.

For a long time, we have been running our application in debug mode. We can enable the debug mode of the application by setting the debug = True parameter. Here we configure it in the run. py script.

It is convenient to develop applications in this way, but we need to disable the debug mode in the production environment. Let's create another STARTUP script file to set off dubug mode (filerunp. py ):
 

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

Restart the application now:

./runp.py

Now, rename the Second Account nick name to 'dup'

We didn't get an error message this time. Instead, we got an HTTP 500 error code, which is an internal server error. Although this is not easy to locate the error, at least no details of our application are exposed to strangers. When debugging is disabled and an exception occurs, Flask generates a 500 page.

Although this is better, there are still two problems. First, the problem of beautification: the default 500 page is ugly. The second problem is more important. When the user fails to perform operations, we cannot get the error message because the error is handled silently in the background. Fortunately, there is a simple way to deal with these two problems.

Custom HTTP Error handling program

Flask provides a mechanism for applications to install their own error pages. As an example, Let's define two custom pages with the most common HTTP 404 and 500 errors. You can also customize other error pages.

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

There is no need to talk about this, because they are self-evident. The only interesting part is the rollack statement in Error 500. This is indispensable because this method will be called as an exception. If an exception is caused by a database error, the database session becomes invalid. Therefore, we need to roll it back to prevent a session from turning to a 500 Error template.

This is a 404 error in the template
 

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

This is a 500 Error template.
 

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

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

Send error logs by email

To handle the second problem, we need to configure the application error reporting mechanism.

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

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

# mail server settingsMAIL_SERVER = 'localhost'MAIL_PORT = 25MAIL_USERNAME = NoneMAIL_PASSWORD = None # administrator listADMINS = ['you@example.com']

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

Flask uses the common Python logging module, so it is very easy to set the mail to send error logs (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 that we are only enabling the emails when we run without debugging.
Note: we want to enable the email function in non-dubug mode.
 

It is also easy to test the mail function on a pc without an email server. Fortunately, Python has an SMTP 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 emails in the console window.

Print logs to files

The Error Log received by email is very good, but this is not enough. Some failed conditions do not trigger exceptions and are not the main issue. Therefore, we need to save the logs to the log file. In some cases, logs must be used for troubleshooting.

For this reason, our application needs a log file.

The file logging and email logging are 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, 10)  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 will be generated under the tmp directory, with the file name microblog. log. The RotatingFileHandler method we use has a parameter that limits the number of logs. In this case, the size of a log file is limited to 1 MB, and the last 10 files are used as backups.
Logging. the Formatter class provides the Definition Format of log information. Since this information will be written to a file, we want to obtain as much information as possible. Therefore, apart from log information and stack information, we also wrote a timestamp, log level, file name, and information line number.


To make logs more useful, we reduce the Log Level of the Application Log and file log processing program, because we will have the opportunity to write useful information into the log without errors. As an example, we set the log level to the information level at startup. From now on, every time you start the application, your debugging information will be recorded.

When logs are not used, it is very difficult to debug an online and in-use web service and write the log information into the file, it will be a useful tool for us to diagnose and solve problems, so now let's be ready to use this function.

Bug fixes

Let's fix the duplicate nickname bug.

As discussed earlier, there are two areas that have not been repeated. The first is the after_login processing of Flask-Login. This method will be called after the User successfully logs on to the system. We need to create a new User instance. This is an affected code snippet. We have fixed it (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()

 
To solve this problem, let the User class select a unique name for us, which is also done by the make_unique_nickname method (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, we recommend that you use "miguel2" in this method, but if it also exists, "miguel3" will be generated "···. Note that we set this method as a static method because this operation is not applicable to any class instance.

The second option that leads to duplicate nicknames is to edit the page view function. This is a small prank for the user to choose nickname. The correct method is that users cannot enter duplicate names and change users to another name. We solve this problem by adding form verification. If you enter an invalid nickname, a field verification failure message will be obtained, to add verification, you only need to override 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).first()    if user != None:      self.nickname.errors.append('This nickname is already in use. Please choose another one.')      return False    return True

 

The form constructor adds a new parameter original_nickname. The verification method validate uses this parameter to determine whether the nickname has been modified. If no modification is made, it is returned directly. If it has already been modified, method to check whether the new nickname already exists in the database.

Next we will 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 template ):

 

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

Now this bug has been fixed to prevent the emergence of duplicate data, unless these verification methods do not work properly. When two or more threads/processes access the database in parallel, this still has a potential problem, but these are the topics we will discuss later.

Here you can try to select a duplicate name to see how the form handles these errors.
 
Unit Test Framework

Let's take a look at the previous test sessions and discuss the topics about automated testing.

As the application grows, it is increasingly difficult to determine whether the code changes will affect the existing functions.

The traditional method to prevent regression is a good method. You can write unit tests to test all the different functions of the application. Each test focuses on one point to verify whether the results are consistent with the expected ones. The test program runs regularly to check whether the application is working properly. When the test coverage rate increases, you can confidently modify and add new features. You only need to test the program to verify whether the existing functions of the application are affected.


Now we use the unittest test component of python 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 TestCase(unittest.TestCase):  def setUp(self):    app.config['TESTING'] = True    app.config['CSRF_ENABLED'] = False    app.config['SQLALCHEMY_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 = 'john', email = 'john@example.com')    avatar = u.avatar(128)    expected = 'http://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'    assert avatar[0:len(expected)] == expected   def test_make_unique_nickname(self):    u = User(nickname = 'john', email = '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. Here we only need to know That TestCase is our test class. The setUp and tearDown methods are somewhat special. They are executed before and after each test method. The settings of complex points can contain several groups of tests, each representing a unit test, the subclass of TestCase and each group will have independent setUp and tearDown methods.


These special setUp and tearDown methods are very common and can be easily modified in setUp. For example, we want to test different databases as the primary database, in tearDown, you only need to set the database content.

Testing is implemented as a method. A test should run some application methods with known results, and be able to assert that the results are different from expected.


So far, there have been two tests in our testing framework. First, verify that Gravatar avatar URLs generated in the previous article is correct. Note that the expected avatar is hard-coded in the test and compared with the objects returned in the User class.

The second test is the test_make_unique_nickname method, which is also in the User class. This test is a bit detailed. It creates a new user and writes it to the database. It also determines the uniqueness of the name. Next, create the second user. We recommend that you use a unique name. You can try to use the first user name. We recommend that you use different names as expected in the second part of the test.


To run this test suite, you only need to run the tests. py script:
 

./tests.py

If an error message is displayed, you will get a report on the console.
Conclusion

So far, I hope this article will be useful to you.

Old Rules: If you have any comments, please write them below.

The code of the Weibo application is updated today. 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.