Slide 1

Slide 1 text

PyConZA 2017 Another angle on test infrastructure Iwan Vosloo

Slide 2

Slide 2 text

PyConZA 2017 Reahl Fixtures TestFunctio n Fixture Person Bank Account https://2016.za.pycon.org/talks/26/

Slide 3

Slide 3 text

PyConZA 2017 Fixture basics class MyFixture(Fixture): def new_person(self, name='John'): return Person(name=name) with MyFixture() as fixture: assert fixture.person.name == 'John' assert fixture.person is fixture.person

Slide 4

Slide 4 text

PyConZA 2017 Beyond defaults class MyFixture(Fixture): def new_person(self, name='John'): return Person(name=name) with MyFixture() as fixture: jane = fixture.new_person(name='Jane') assert jane is not fixture.person

Slide 5

Slide 5 text

PyConZA 2017 Simplifying dependencies class MyFixture(Fixture): def new_person(self, name='John'): return Person(name=name) def new_bank_account(number='123', person=None): return BankAccount(number=number, owner=person or self.person) with MyFixture() as fixture: assert fixture.bank_account.owner is fixture.person

Slide 6

Slide 6 text

PyConZA 2017 Set up & Tear down class MyFixture(Fixture): @set_up def start_server(self): ... with MyFixture() as fixture: assert fixture.bank_account.owner is fixture.person set_up() tear_down()

Slide 7

Slide 7 text

PyConZA 2017 pytest integration class MyFixture(Fixture): def new_person(self, name='John'): return Person(name=name) @with_fixtures(MyFixture) def test_something(fixture): assert fixture.person.name == 'John' assert fixture.person is fixture.person

Slide 8

Slide 8 text

PyConZA 2017 Multiple Fixtures class MyFixture(Fixture): def new_person(self, name='John'): return Person(name=name) @with_fixtures(MyFixture, AnotherFixture) def test_something(fixture, another): assert fixture.person.name == 'John' assert fixture.person is fixture.person login = another.new_login(fixture.person) ...

Slide 9

Slide 9 text

PyConZA 2017 Dependent Fixtures class PartyFixture(Fixture): def new_person(self, name='John'): return Person(name=name) @uses(parties=PartyFixture) class BankingFixture(Fixture) def new_bank_account(number='123', person=None): return BankAccount(number=number, owner=person or self.parties.person) @with_fixtures(BankingFixture) def test_something(banking): assert fixture.bank_account.owner is fixture.parties.person

Slide 10

Slide 10 text

PyConZA 2017 Fixture scope @scope('session') class Database(Fixture): def new_connection(self): ... @uses(database=Database) class PartyFixture(Fixture): def new_person(self, name='John'): # do stuff with self.database.connection # (which is only created once per session)

Slide 11

Slide 11 text

PyConZA 2017 Motivations ● No magic - its all Python and imports ● Each Fixture has responsibility for things that belong together ● Dependencies on other Fixtures that have other responsibilities ● The (other Fixture) dependencies of a Fixture is its own business ● Able to re-use Fixtures without having to inherit

Slide 12

Slide 12 text

PyConZA 2017 Configuration and database «session» ReahlSystem SessionFixture @set_up(connect_db) @tear_down(disconnect_db) system_control config Configuration

Slide 13

Slide 13 text

PyConZA 2017 Isolating state @with_fixtures( ReahlSystemFixture ) def test_things( reahl_system ): reahl_system.config.some_setting = 'changed'

Slide 14

Slide 14 text

PyConZA 2017 Isolating state «session» ReahlSystem SessionFixture @set_up(connect_db) @tear_down(disconnect_db) system_control config «function» ReahlSystem Fixture system_control config Configuration Configuration copy of

Slide 15

Slide 15 text

PyConZA 2017 Controlling SqlAlchemy «session» ReahlSystem SessionFixture «function» ReahlSystem Fixture «function» SqlAlchemyFixture @set_up(start_transaction) @tear_down(abort_transaction) persistent_test_classes

Slide 16

Slide 16 text

PyConZA 2017 Using SqlAlchemy @with_fixtures( SqlAlchemyFixture ) def test_things( sql_alchemy ): Session.add( Person(name='Jane') ) assert Session.query( Person ).count() == 1

Slide 17

Slide 17 text

PyConZA 2017 Temporary tables @with_fixtures( SqlAlchemyFixture ) def test_things( sql_alchemy ): class MyTestObject( Base ): __tablename__ == 'my_object' name = Column( String, primary_key=True ) def __init__(self, name): self.name = name with sql_alchemy.persistent_test_classes( MyTestObject ): Session.add( MyTestObject(name='A') ) assert Session.query( MyTestObject ).count() == 1

Slide 18

Slide 18 text

PyConZA 2017 The web server web browser pytest process web server process database selenium

Slide 19

Slide 19 text

PyConZA 2017 Separate server transaction pytest process pytest transaction web browser web server web server transaction create_objects() commit() visit_web_page() modify_objects() http_get() commit() check_modified_ objects()

Slide 20

Slide 20 text

PyConZA 2017 Server-side breakage pytest process web browser web server visit_web_page() http_get() breaks http_500() check_web_page()

Slide 21

Slide 21 text

PyConZA 2017 Integrating the web server web browser database pytest code web server code selenium

Slide 22

Slide 22 text

PyConZA 2017 Fixtures for web «session» WebServerFixture @set_up(start_servers) @tear_down(stop_servers) reahl_server web_driver «function» WebFixture reahl_server driver_browser log_in «function» PartyAccount Fixture person system_account

Slide 23

Slide 23 text

PyConZA 2017 File uploads example

Slide 24

Slide 24 text

PyConZA 2017 File uploads example

Slide 25

Slide 25 text

PyConZA 2017 File uploads example

Slide 26

Slide 26 text

PyConZA 2017 Testing file uploads @with_fixtures(WebFixture, FileUploadInputFixture) def test_file_upload_input_basics(web_fixture, fixture): web_fixture.reahl_server.set_app(fixture.wsgi_app) browser = web_fixture.driver_browser assert not fixture.file_was_uploaded( fixture.file_to_upload1.name ) assert not fixture.file_was_uploaded( fixture.file_to_upload2.name ) # Upload one file browser.type(XPath.input_labelled('Choose file(s)'), fixture.file_to_upload1.name) browser.click(XPath.button_labelled('Upload')) assert fixture.file_was_uploaded( fixture.file_to_upload1.name ) assert not fixture.file_was_uploaded( fixture.file_to_upload2.name )

Slide 27

Slide 27 text

PyConZA 2017 Testing file uploads @with_fixtures(WebFixture, FileUploadInputFixture) def test_file_upload_input_basics(web_fixture, fixture): web_fixture.reahl_server.set_app(fixture.wsgi_app) browser = web_fixture.driver_browser assert not fixture.file_was_uploaded( fixture.file_to_upload1.name ) assert not fixture.file_was_uploaded( fixture.file_to_upload2.name ) # Upload one file browser.type(XPath.input_labelled('Choose file(s)'), fixture.file_to_upload1.name) browser.click(XPath.button_labelled('Upload')) assert fixture.file_was_uploaded( fixture.file_to_upload1.name ) assert not fixture.file_was_uploaded( fixture.file_to_upload2.name )

Slide 28

Slide 28 text

PyConZA 2017 File uploads example

Slide 29

Slide 29 text

PyConZA 2017 Beyond the rule assert not fixture.file_was_uploaded( fixture.file_to_upload1.name ) assert not fixture.uploaded_file_is_listed( fixture.file_to_upload1.name ) with web_fixture.reahl_server.in_background(wait_till_done_serving=False): # Upload will block, see fixture: browser.type(XPath.input_labelled('Choose file(s)'), fixture.file_to_upload1.name) assert fixture.progess_bar_is_displayed( fixture.file_to_upload1.name ) browser.click(XPath.button_labelled('Cancel')) assert not fixture.uploaded_file_is_listed( fixture.file_to_upload1.name ) assert not fixture.file_was_uploaded( fixture.file_to_upload1.name )

Slide 30

Slide 30 text

PyConZA 2017 The ExecutionContext ● Who is logged in? ● What locale to use to decide the current language etc. ● Various system configuration settings. ● The current database connection / transaction / Session ● (The current request)

Slide 31

Slide 31 text

PyConZA 2017 Finding it ExecutionContext context = ExecutionContext.get_context() context.config.web.default_url_locale context.config.web.default_http_port

Slide 32

Slide 32 text

PyConZA 2017 Behind the scenes... @with_fixtures( SqlAlchemyFixture ) def test_things( sql_alchemy ): Session.add( Person(name='Jane') ) assert Session.query( Person ).count() == 1 context = ExecutionContext.get_context()

Slide 33

Slide 33 text

PyConZA 2017 Foundational Fixtures «session» ReahlSystem SessionFixture «function» ReahlSystem Fixture «function» SqlAlchemyFixture «session» WebServerFixture «function» WebFixture «function» PartyAccount Fixture «function» ContextAware Fixture

Slide 34

Slide 34 text

PyConZA 2017 Lingering issues ● changing implementations of browsers ● switching javascript on and off ● testing browser GUI stuff ● CSS effects and layout

Slide 35

Slide 35 text

PyConZA 2017 Thank you On groups.google.com: ● reahl-discuss www.reahl.org [email protected] Slides: https://goo.gl/sGtmcE