librelist archives

« back to archive

Flaskesque unit testing

Flaskesque unit testing

From:
Dag Odenhall
Date:
2010-11-10 @ 20:48
I have this idea for a Flask-inspired unit testing API, is it worth
pursuing?



math = Tests()  # similar to a TestCase or flask.Module

@math.context
def context():
    setup()
    yield  # could yield values to test args, or use context-locals
    teardown()

@math.test
def arithmetics():
    assert_equal(1 + 1, 2)

suite = Suite()  # similar to TestSuite or flask.Flask
suite.register(math)

if __name__ == '__main__':
    suite.run()



I want to treat testing as just another type of package. We'd usually
consider it bad to, say, autoregister view modules in a web framework,
or rely on naming for classifying objects.

I also have this idea for dict-configured multi-assertions, something
like:



assert_all_attributes(response, {
    'status_code': 200,
    'content_type': 'text/html',
    'data:in': '<h1>Hello World!</h1>'
})



What do you think?

Re: Flaskesque unit testing

From:
Dag Odenhall
Date:
2010-11-25 @ 21:54
I just released version 0.1 of this, considered alpha status. Please
test it out if you're interested, and report any bugs, questions or
ideas you have.

PyPI: http://pypi.python.org/pypi/Attest/
Docs: http://packages.python.org/Attest/
GitHub: http://github.com/dag/attest/

Please keep in mind that it's a first release and in alpha-status - I'm
very open to suggestions for improvements.

Dag

Re: [flask] Re: Flaskesque unit testing

From:
danjac354@gmail.com
Date:
2010-11-25 @ 22:31
Looks very interesting - I'm not a great fan of unittest but have used
it for pragmatic reasons - this looks like it might become a good
alternative.

This also looks like it might work very nicely with Flask. You could
create a @context decorator function to return a Flask request
context, for example. The existing Flask-Testing unittest methods
(assert_404 etc) could be ported over quite nicely.

It wasn't quite clear from the docs, but how do you manage a
"teardown" ? For example, if I need to create/destroy a test database
?

On 25 November 2010 21:54, Dag Odenhall <dag.odenhall@gmail.com> wrote:
> I just released version 0.1 of this, considered alpha status. Please test it
> out if you're interested, and report any bugs, questions or ideas you have.
>
> PyPI: http://pypi.python.org/pypi/Attest/
> Docs: http://packages.python.org/Attest/
> GitHub: http://github.com/dag/attest/
>
> Please keep in mind that it's a first release and in alpha-status - I'm very
> open to suggestions for improvements.
>
> Dag

Re: [flask] Re: Flaskesque unit testing

From:
Dag Odenhall
Date:
2010-11-25 @ 22:56
On Thu, 2010-11-25 at 22:31 +0000, danjac354@gmail.com wrote:
> This also looks like it might work very nicely with Flask. You could
> create a @context decorator function to return a Flask request
> context, for example.

I want to make it easier to "inherit" a context. Currently you have to
do this whole dance,

@my.context
def context():
    with imported_context():
        yield

Which seems a little overkill to me, would be nice to easily subclass
Tests and add a default context, i.e. a FlaskTests that runs in a
test_request_context.

> The existing Flask-Testing unittest methods
> (assert_404 etc) could be ported over quite nicely.

Continuing on the previous point, I'd like a Werkzeug test client that
returns the result wrapped in Assert. Such a client could be yielded
from the default context. This makes it easy to assert things like the
status code.

api = FlaskTests()

@api.test
def returns_json(client):
    response = client.get('/api/')
    assert response.status_code == 200
    assert response.mimetype == 'application/json'

In this example, 'response' is a response object wrapped in
attest.Assert. Note that the 'assert' keywords here are optional, I just
add them for clarity/readability and to avoid some potential silently
passing tests. The actual assertions are done in Assert.__eq__.

> It wasn't quite clear from the docs, but how do you manage a
> "teardown" ? For example, if I need to create/destroy a test database?

They're just context managers, so just do stuff after the yield. Might
need to wrap in try-finally though *shouldn't* be needed as Attest will
catch all exceptions anyway. You can also yield from a context manager
such as contextlib.closing() or something similar built into your DB
toolkit.

@my.context
def context():
    with db.open() as conn:
        yield conn

A similar example exists in the docs:
http://packages.python.org/Attest/api.html#attest.Tests.context

Re: [flask] Re: Flaskesque unit testing

From:
danjac354@gmail.com
Date:
2010-11-25 @ 23:18
Flask-Testing currently does some syntactic sugar (assert_404 etc)
which can be ported over as simple functions (or just do assert
response.status_code == 404 etc, if you prefer).

It also provides a few utilities e.g. to help with JSON testing. This
requires e.g. being able to set the Response class.

It should be quite doable as you describe, something along these lines:

from flaskext.testing import Assert, assert_200

from myproject import app # instance or factory function

api = Assert(app)

@api.context
def test_something(client):
     response = client.get("/")
     assert_200(response)

There would need to be an easy way to hook into the context manager.
For example, in addition to setting up the test_request_context, I
might need to do some DB setup. In SQLAlchemy for example I need to
create/drop the db, remove the current session, etc.

So we might need something like this:

@api.before
def before():
    db.create_db()

@api.after
def after()
    db.session.remove()
    db.drop_db()

These would need to be run inside of the test_request_context call.

On 25 November 2010 22:56, Dag Odenhall <dag.odenhall@gmail.com> wrote:
> On Thu, 2010-11-25 at 22:31 +0000, danjac354@gmail.com wrote:
>> This also looks like it might work very nicely with Flask. You could
>> create a @context decorator function to return a Flask request
>> context, for example.
>
> I want to make it easier to "inherit" a context. Currently you have to
> do this whole dance,
>
> @my.context
> def context():
>    with imported_context():
>        yield
>
> Which seems a little overkill to me, would be nice to easily subclass
> Tests and add a default context, i.e. a FlaskTests that runs in a
> test_request_context.
>
>> The existing Flask-Testing unittest methods
>> (assert_404 etc) could be ported over quite nicely.
>
> Continuing on the previous point, I'd like a Werkzeug test client that
> returns the result wrapped in Assert. Such a client could be yielded
> from the default context. This makes it easy to assert things like the
> status code.
>
> api = FlaskTests()
>
> @api.test
> def returns_json(client):
>    response = client.get('/api/')
>    assert response.status_code == 200
>    assert response.mimetype == 'application/json'
>
> In this example, 'response' is a response object wrapped in
> attest.Assert. Note that the 'assert' keywords here are optional, I just
> add them for clarity/readability and to avoid some potential silently
> passing tests. The actual assertions are done in Assert.__eq__.
>
>> It wasn't quite clear from the docs, but how do you manage a
>> "teardown" ? For example, if I need to create/destroy a test database?
>
> They're just context managers, so just do stuff after the yield. Might
> need to wrap in try-finally though *shouldn't* be needed as Attest will
> catch all exceptions anyway. You can also yield from a context manager
> such as contextlib.closing() or something similar built into your DB
> toolkit.
>
> @my.context
> def context():
>    with db.open() as conn:
>        yield conn
>
> A similar example exists in the docs:
> http://packages.python.org/Attest/api.html#attest.Tests.context
>
>

Re: [flask] Re: Flaskesque unit testing

From:
Dag Odenhall
Date:
2010-11-25 @ 23:35
On Thu, 2010-11-25 at 23:18 +0000, danjac354@gmail.com wrote:

> There would need to be an easy way to hook into the context manager.
> For example, in addition to setting up the test_request_context, I
> might need to do some DB setup. In SQLAlchemy for example I need to
> create/drop the db, remove the current session, etc. 



@api.context
def context():
    with app.test_request_context():
        db.create_db()
        yield
        db.session.remove()
        db.drop_db()


If 'test_request_context' was made a default context for a FlaskTests
class or similar, you could also drop that with statement. I just had an
idea how to do that, the Tests class could inject all contexts in a
contexts attribute and then you could subclass like so:


class FlaskTests(Tests):
    contexts = (app.test_request_context,)


And to generalize for any app,


class FlaskTests(Tests):

    def __init__(self, app):
        Tests.__init__(self)
        self.contexts = (app.test_request_context,)


Not sure yet if this is the best idea, but it's one idea. A contrived
example obviously, the real thing would handle app factories and
probably a nice way to subclass FlaskTests too for setting your
application so you don't have to do it for each FlaskTests instance.

Re: [flask] Re: Flaskesque unit testing

From:
Dag Odenhall
Date:
2010-11-26 @ 17:26
On Fri, 2010-11-26 at 00:35 +0100, Dag Odenhall wrote:
> Not sure yet if this is the best idea, but it's one idea.

Pushed a different solution: you can have multiple @tests.context. This
allows the use of factory functions:


def flask_tests(app):
    tests = Tests()
    @tests.context
    def request_context():
        with app.test_request_context():
            yield Assert(app.test_client())
    return tests


api = flask_tests(app)

@api.context
def not_overiding(): pass

@api.test
def json(client):
    response = client.get('/api/')
    response.mimetype == 'application/json'
    response.status_code == 200


You could also have helper functions for adding contexts with less
boilerplate:


def with_db(tests):
    @tests.context
    def connect():
        with db.open() as conn:
            yield conn

api = flask_tests(app)
with_db(api)

@api.test
def json(client, conn): pass


Is this a good solution? It feels right to me, and in relation to Flask
and the tradition of create_app() and init_app() etc. 

Re: [flask] Re: Flaskesque unit testing

From:
Zahari Petkov
Date:
2010-11-25 @ 23:09
One quick issue I thought of: I definitely like the concept with
operator overloading of the Assert object, but the name is a bit off I
think - it is a verb, and at the same time it tries to be something
different from a normal assert, but sticks with the same name. How
about Asserter, or maybe something even more different?

Thanks,
Zahari

On Thu, Nov 25, 2010 at 11:54 PM, Dag Odenhall <dag.odenhall@gmail.com> wrote:
> I just released version 0.1 of this, considered alpha status. Please test it
> out if you're interested, and report any bugs, questions or ideas you have.
>
> PyPI: http://pypi.python.org/pypi/Attest/
> Docs: http://packages.python.org/Attest/
> GitHub: http://github.com/dag/attest/
>
> Please keep in mind that it's a first release and in alpha-status - I'm very
> open to suggestions for improvements.
>
> Dag

Re: [flask] Re: Flaskesque unit testing

From:
Dag Odenhall
Date:
2010-11-25 @ 23:17
On Fri, 2010-11-26 at 01:09 +0200, Zahari Petkov wrote:
> One quick issue I thought of: I definitely like the concept with
> operator overloading of the Assert object, but the name is a bit off I
> think - it is a verb, and at the same time it tries to be something
> different from a normal assert, but sticks with the same name. How
> about Asserter, or maybe something even more different? 

It's verb-like when used like,

Assert(result) == expected

For situations where you wrap 'result' before asserting, 'Assertive'
would be a better name. You can always 'import as'…

Re: [flask] Re: Flaskesque unit testing

From:
Zahari Petkov
Date:
2010-11-25 @ 23:13
BTW, I particularly liked this use case:

hello = Assert('hello')
hello == 'hello'
hello.upper() == 'HELLO'
hello.capitalize() == 'Hello'

Very impressive.

On Fri, Nov 26, 2010 at 1:09 AM, Zahari Petkov <zarchaoz@gmail.com> wrote:
> One quick issue I thought of: I definitely like the concept with
> operator overloading of the Assert object, but the name is a bit off I
> think - it is a verb, and at the same time it tries to be something
> different from a normal assert, but sticks with the same name. How
> about Asserter, or maybe something even more different?
>
> Thanks,
> Zahari
>
> On Thu, Nov 25, 2010 at 11:54 PM, Dag Odenhall <dag.odenhall@gmail.com> wrote:
>> I just released version 0.1 of this, considered alpha status. Please test it
>> out if you're interested, and report any bugs, questions or ideas you have.
>>
>> PyPI: http://pypi.python.org/pypi/Attest/
>> Docs: http://packages.python.org/Attest/
>> GitHub: http://github.com/dag/attest/
>>
>> Please keep in mind that it's a first release and in alpha-status - I'm very
>> open to suggestions for improvements.
>>
>> Dag
>

Re: [flask] Flaskesque unit testing

From:
danjac354@gmail.com
Date:
2010-11-10 @ 20:53
I started on something like this in a branch of Flask-Testing - there
might be something of use to you:

http://bitbucket.org/danjac/flask-testing/src/37863a0d955a/flaskext/testing.py


On 10 November 2010 20:48, Dag Odenhall <dag.odenhall@gmail.com> wrote:
> I have this idea for a Flask-inspired unit testing API, is it worth
> pursuing?
>
>
> math = Tests()  # similar to a TestCase or flask.Module
>
> @math.context
> def context():
>     setup()
>     yield  # could yield values to test args, or use context-locals
>     teardown()
>
> @math.test
> def arithmetics():
>     assert_equal(1 + 1, 2)
>
> suite = Suite()  # similar to TestSuite or flask.Flask
> suite.register(math)
>
> if __name__ == '__main__':
>     suite.run()
>
>
> I want to treat testing as just another type of package. We'd usually
> consider it bad to, say, autoregister view modules in a web framework, or
> rely on naming for classifying objects.
>
> I also have this idea for dict-configured multi-assertions, something like:
>
>
> assert_all_attributes(response, {
>     'status_code': 200,
>     'content_type': 'text/html',
>     'data:in': '<h1>Hello World!</h1>'
> })
>
>
> What do you think?