Testing file uploads in Flask

Question:

I’m using Flask-Testing for my Flask integration tests. I’ve got a form that has a file upload for a logo that I’m trying to write tests for but I keep getting an error saying: TypeError: 'str' does not support the buffer interface.

I’m using Python 3. The closest answer I have found is this but it’s not working for me.

This is what one of my many attempts looks like:

def test_edit_logo(self):
    """Test can upload logo."""
    data = {'name': 'this is a name', 'age': 12}
    data['file'] = (io.BytesIO(b"abcdef"), 'test.jpg')
    self.login()
    response = self.client.post(
        url_for('items.save'), data=data, follow_redirects=True)
    })
    self.assertIn(b'Your item has been saved.', response.data)
    advert = Advert.query.get(1)
    self.assertIsNotNone(item.logo)

How does one test a file upload in Flask?

Asked By: hammygoonan

||

Answers:

You need two things:

1.) content_type='multipart/form-data' in your .post()
2.) in your data= pass in file=(BytesIO(b'my file contents'), "file_name.jpg")

A full example:

    data = dict(
        file=(BytesIO(b'my file contents'), "work_order.123"),
    )

    response = app.post(url_for('items.save'), content_type='multipart/form-data', data=data)
Answered By: mmcclannahan

The issue ended up not being that when one adds content_type='multipart/form-data' to the post method it expect all values in data to either be files or strings. There were integers in my data dict which I realised thanks to this comment.

So the end solution ended up looking like this:

def test_edit_logo(self):
    """Test can upload logo."""
    data = {'name': 'this is a name', 'age': 12}
    data = {key: str(value) for key, value in data.items()}
    data['file'] = (io.BytesIO(b"abcdef"), 'test.jpg')
    self.login()
    response = self.client.post(
        url_for('adverts.save'), data=data, follow_redirects=True,
        content_type='multipart/form-data'
    )
    self.assertIn(b'Your item has been saved.', response.data)
    advert = Item.query.get(1)
    self.assertIsNotNone(item.logo)
Answered By: hammygoonan

You can use Werkzeug’s FileStorage (as used by Flask under the hood) which you don’t need to install as it comes with Flask.

You can mock a file like this:

import io
import json

from werkzeug.datastructures import FileStorage

# Here we are mocking a JSON file called Input.json
my_dict = {"msg": "hello!"}
input_json = json.dumps(my_dict, indent=4).encode("utf-8")

mock_file = FileStorage(
    stream=io.BytesIO(input_json),
    filename="Input.json",
    content_type="application/json",
)

This example uses a real file to test against:

from werkzeug.datastructures import FileStorage


my_file = FileStorage(
    stream=open("tests/assets/my_video.mp4", "rb"),
    filename="my_video.mp4",
    content_type="video/mpeg",
)

rv = client.post(
   "/api/v1/video",
   data={
      "my_video": my_file,
   },
   content_type="multipart/form-data"
)

Test to see it returns a response status code of 200:

assert "200" in rv.status

I can then test that the file arrives in a test directory on the server:

assert "my_video.mp4" in os.listdir("tests/my_test_path")

Also note, you need to set the mocked file to None on teardown otherwise you’ll get a ValueError: I/O operation on closed file. . Below is a Pytest example:

    def setup_method(self):
        self.mock_file = FileStorage(
            stream=io.BytesIO(input_json),
            filename="Input.json",
            content_type="application/json",
        )

    def teardown_method(self):
        self.mock_file = None
Answered By: Joe Gasewicz

While trying to find a bug in my code I’ve created a SSCCE for file upload (based on the docs) with a corresponding test based on other answers here. It might be useful for somebody:

app.py:

import base64
import os
import pathlib
import tempfile
import textwrap

import flask
import werkzeug.utils

root = flask.Blueprint('root', __name__)

@root.route('/', methods=['GET', 'POST'])
def upload_file():
    if flask.request.method == 'POST':
        try:
            file = flask.request.files['file']
            if not file.filename:
                raise LookupError()
            filename = werkzeug.utils.secure_filename(file.filename)
            file.save(pathlib.Path(flask.current_app.config['UPLOAD_FOLDER'], filename))
            flask.flash('File saved!', 'message')
        except LookupError:
            flask.flash('No file provided!', 'error')
        return flask.redirect(flask.url_for('root.upload_file'))
    else:
        return flask.render_template_string(textwrap.dedent(
            '''
            <!doctype html>
            <title>Upload new File</title>
            {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
            <ul class=flashes>
            {% for category, message in messages %}<li class="{{ category }}">{{ message }}</li>
            {% endfor %}
            </ul>
            {% endif %}{% endwith %}
            <h1>Upload new File</h1>
            <form method=post enctype=multipart/form-data>
            <input type=file name=file>
            <input type=submit value=Upload>
            </form>
            '''
        ))

def create_app():
    app = flask.Flask(__name__)
    app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
    app.secret_key = 'change-me'
    app.register_blueprint(root)
    return app

if __name__ == '__main__':
    create_app()

test_app.py:

"""upload tests"""

import base64
import io
import unittest

import werkzeug

import app

# https://raw.githubusercontent.com/mathiasbynens/small/master/jpeg.jpg
SMALLEST_JPEG_B64 = """
/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=
"""

class BaseTestCase(unittest.TestCase):
    def test_save(self):
        with app.create_app().test_client() as client:
            file = werkzeug.datastructures.FileStorage(
                stream=io.BytesIO(base64.b64decode(SMALLEST_JPEG_B64)),
                filename="example image.jpg",
                content_type="image/jpg",
            )
            response = client.post(
                '/',
                data=dict(
                    file=file,
                ),
                follow_redirects=True,
                content_type='multipart/form-data',
            )
Answered By: Tometzky
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.