├── .gitignore ├── .pylintrc ├── .travis.yml ├── README.md ├── requirements.txt ├── section10 ├── README.md ├── code │ ├── app.py │ ├── requirements.txt │ ├── templates │ │ ├── blog.html │ │ ├── home.html │ │ ├── new_post.html │ │ └── post.html │ └── tests │ │ ├── __init__.py │ │ └── acceptance │ │ ├── __init__.py │ │ ├── content.feature │ │ ├── locators │ │ ├── __init__.py │ │ ├── base_page.py │ │ ├── blog_page.py │ │ ├── home_page.py │ │ └── new_post_page.py │ │ ├── navigation.feature │ │ ├── page_model │ │ ├── __init__.py │ │ ├── base_page.py │ │ ├── blog_page.py │ │ ├── home_page.py │ │ └── new_post_page.py │ │ └── steps │ │ ├── __init__.py │ │ ├── content.py │ │ ├── interactions.py │ │ ├── navigation.py │ │ └── waits.py └── video_code │ ├── README.md │ ├── app.py │ ├── requirements.txt │ ├── templates │ ├── blog.html │ ├── home.html │ ├── new_post.html │ └── post.html │ └── tests │ ├── __init__.py │ └── acceptance │ ├── __init__.py │ ├── content.feature │ ├── navigation.feature │ ├── page_model │ ├── __init__.py │ ├── base_page.py │ ├── blog_page.py │ ├── home_page.py │ ├── new_post_page.py │ └── post_page.py │ ├── pages │ ├── __init__.py │ ├── base_page.py │ ├── blog_page.py │ ├── home_page.py │ ├── new_post_page.py │ └── post_page.py │ └── steps │ ├── __init__.py │ ├── content.py │ ├── interaction.py │ ├── navigation.py │ └── waits.py ├── section3 └── video_code │ ├── app.py │ ├── blog.py │ ├── post.py │ └── tests │ ├── __init__.py │ ├── integration │ ├── __init__.py │ └── blog_test.py │ ├── system │ ├── __init__.py │ └── app_test.py │ └── unit │ ├── __init__.py │ ├── blog_test.py │ └── post_test.py ├── section4 └── video_code │ ├── __init__.py │ ├── app.py │ ├── requirements.txt │ ├── templates │ └── home.html │ └── tests │ ├── __init__.py │ └── system │ ├── __init__.py │ ├── base_test.py │ └── test_home.py ├── section5 ├── starter_code │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── models │ │ ├── __init__.py │ │ └── item.py │ ├── readme.md │ ├── requirements.txt │ ├── resources │ │ ├── __init__.py │ │ └── item.py │ ├── run.py │ └── tests │ │ └── __init__.py └── video_code │ ├── Procfile │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── models │ ├── __init__.py │ └── item.py │ ├── readme.md │ ├── requirements.txt │ ├── resources │ ├── __init__.py │ └── item.py │ ├── run.py │ ├── runtime.txt │ ├── tests │ ├── __init__.py │ ├── base_test.py │ ├── integration │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ └── item_test.py │ └── unit │ │ ├── __init__.py │ │ └── models │ │ ├── __init__.py │ │ └── item_test.py │ └── uwsgi.ini ├── section6 ├── starter_code │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── models │ │ ├── __init__.py │ │ ├── item.py │ │ └── store.py │ ├── readme.md │ ├── requirements.txt │ ├── resources │ │ ├── __init__.py │ │ ├── item.py │ │ └── store.py │ ├── run.py │ └── tests │ │ ├── __init__.py │ │ ├── base_test.py │ │ ├── integration │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ └── item_test.py │ │ └── unit │ │ ├── __init__.py │ │ └── models │ │ ├── __init__.py │ │ └── item_test.py └── video_code │ ├── Procfile │ ├── README.md │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── models │ ├── __init__.py │ ├── item.py │ └── store.py │ ├── requirements.txt │ ├── resources │ ├── __init__.py │ ├── item.py │ └── store.py │ ├── run.py │ ├── runtime.txt │ ├── tests │ ├── __init__.py │ ├── base_test.py │ ├── integration │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── item_test.py │ │ │ └── store_test.py │ └── unit │ │ ├── __init__.py │ │ └── models │ │ ├── __init__.py │ │ ├── item_test.py │ │ └── store_test.py │ └── uwsgi.ini ├── section7 └── video_code │ ├── Procfile │ ├── README.md │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── models │ ├── __init__.py │ ├── item.py │ ├── store.py │ └── user.py │ ├── requirements.txt │ ├── resources │ ├── __init__.py │ ├── item.py │ ├── store.py │ └── user.py │ ├── run.py │ ├── runtime.txt │ ├── security.py │ ├── tests │ ├── __init__.py │ ├── base_test.py │ ├── integration │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── item_test.py │ │ │ ├── store_test.py │ │ │ └── user_test.py │ ├── system │ │ ├── __init__.py │ │ ├── item_test.py │ │ ├── store_test.py │ │ └── user_test.py │ └── unit │ │ ├── __init__.py │ │ └── models │ │ ├── __init__.py │ │ ├── item_test.py │ │ ├── store_test.py │ │ └── user_test.py │ └── uwsgi.ini ├── section8 ├── README.md ├── assets │ ├── overview-of-running-tests-with-newman-export-22-03-2024-11_38_55.png │ └── pycharm-run-configuration-flow-export-22-03-2024-11_38_55.png ├── stores-rest-api.postman_collection.json ├── stores-rest-api.postman_environment.json └── video_code │ ├── Procfile │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── models │ ├── __init__.py │ ├── item.py │ ├── store.py │ └── user.py │ ├── readme.md │ ├── requirements.txt │ ├── resources │ ├── __init__.py │ ├── item.py │ ├── store.py │ └── user.py │ ├── run.py │ ├── runtime.txt │ ├── security.py │ ├── tests │ ├── __init__.py │ ├── base_test.py │ ├── integration │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── item_test.py │ │ │ ├── store_test.py │ │ │ └── user_test.py │ ├── system │ │ ├── __init__.py │ │ ├── item_test.py │ │ ├── store_test.py │ │ └── user_test.py │ └── unit │ │ ├── __init__.py │ │ └── models │ │ ├── __init__.py │ │ ├── item_test.py │ │ ├── store_test.py │ │ └── user_test.py │ └── uwsgi.ini └── testing-python-apps.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.screenflow 3 | *.png 4 | starter_code.zip 5 | .idea/ 6 | .DS_Store 7 | __pycache__/ 8 | *.pyc 9 | exports/ 10 | presentations/ 11 | videos/ 12 | new_videos/ 13 | .vscode/ 14 | venv/ 15 | .venv/ 16 | testing-python-apps.txt 17 | theme.key 18 | data.db 19 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence=HIGH,INFERENCE,INFERENCE_FAILURE 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=yes 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [BASIC] 100 | 101 | # Good variable names which should always be accepted, separated by a comma 102 | good-names=i,j,k,ex,Run,_ 103 | 104 | # Bad variable names which should always be refused, separated by a comma 105 | bad-names=foo,bar,baz,toto,tutu,tata 106 | 107 | # Colon-delimited sets of names that determine each other's naming style when 108 | # the name regexes allow several styles. 109 | name-group= 110 | 111 | # Include a hint for the correct naming format with invalid-name 112 | include-naming-hint=no 113 | 114 | # List of decorators that produce properties, such as abc.abstractproperty. Add 115 | # to this list to register other decorators that produce valid properties. 116 | property-classes=abc.abstractproperty 117 | 118 | # Regular expression matching correct function names 119 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 120 | 121 | # Naming hint for function names 122 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 123 | 124 | # Regular expression matching correct variable names 125 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 126 | 127 | # Naming hint for variable names 128 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 129 | 130 | # Regular expression matching correct constant names 131 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 132 | 133 | # Naming hint for constant names 134 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 135 | 136 | # Regular expression matching correct attribute names 137 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 138 | 139 | # Naming hint for attribute names 140 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 141 | 142 | # Regular expression matching correct argument names 143 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Naming hint for argument names 146 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression matching correct class attribute names 149 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 150 | 151 | # Naming hint for class attribute names 152 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 153 | 154 | # Regular expression matching correct inline iteration names 155 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 156 | 157 | # Naming hint for inline iteration names 158 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 159 | 160 | # Regular expression matching correct class names 161 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 162 | 163 | # Naming hint for class names 164 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 165 | 166 | # Regular expression matching correct module names 167 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 168 | 169 | # Naming hint for module names 170 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 171 | 172 | # Regular expression matching correct method names 173 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 174 | 175 | # Naming hint for method names 176 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx= 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [ELIF] 188 | 189 | # Maximum number of nested blocks for function / method body 190 | max-nested-blocks=5 191 | 192 | 193 | [FORMAT] 194 | 195 | # Maximum number of characters on a single line. 196 | max-line-length=100 197 | 198 | # Regexp for a line that is allowed to be longer than the limit. 199 | ignore-long-lines=^\s*(# )??$ 200 | 201 | # Allow the body of an if to be on the same line as the test if there is no 202 | # else. 203 | single-line-if-stmt=no 204 | 205 | # List of optional constructs for which whitespace checking is disabled. `dict- 206 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 207 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 208 | # `empty-line` allows space-only lines. 209 | no-space-check=trailing-comma,dict-separator 210 | 211 | # Maximum number of lines in a module 212 | max-module-lines=1000 213 | 214 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 215 | # tab). 216 | indent-string=' ' 217 | 218 | # Number of spaces of indent required inside a hanging or continued line. 219 | indent-after-paren=4 220 | 221 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 222 | expected-line-ending-format= 223 | 224 | 225 | [LOGGING] 226 | 227 | # Logging modules to check that the string format arguments are in logging 228 | # function parameter format 229 | logging-modules=logging 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [SIMILARITIES] 239 | 240 | # Minimum lines number of a similarity. 241 | min-similarity-lines=4 242 | 243 | # Ignore comments when computing similarities. 244 | ignore-comments=yes 245 | 246 | # Ignore docstrings when computing similarities. 247 | ignore-docstrings=yes 248 | 249 | # Ignore imports when computing similarities. 250 | ignore-imports=no 251 | 252 | 253 | [SPELLING] 254 | 255 | # Spelling dictionary name. Available dictionaries: none. To make it working 256 | # install python-enchant package. 257 | spelling-dict= 258 | 259 | # List of comma separated words that should not be checked. 260 | spelling-ignore-words= 261 | 262 | # A path to a file that contains private dictionary; one word per line. 263 | spelling-private-dict-file= 264 | 265 | # Tells whether to store unknown words to indicated private dictionary in 266 | # --spelling-private-dict-file option instead of raising a message. 267 | spelling-store-unknown-words=no 268 | 269 | 270 | [TYPECHECK] 271 | 272 | # Tells whether missing members accessed in mixin class should be ignored. A 273 | # mixin class is detected if its name ends with "mixin" (case insensitive). 274 | ignore-mixin-members=yes 275 | 276 | # List of module names for which member attributes should not be checked 277 | # (useful for modules/projects where namespaces are manipulated during runtime 278 | # and thus existing member attributes cannot be deduced by static analysis. It 279 | # supports qualified module names, as well as Unix pattern matching. 280 | ignored-modules= 281 | 282 | # List of class names for which member attributes should not be checked (useful 283 | # for classes with dynamically set attributes). This supports the use of 284 | # qualified names. 285 | ignored-classes=optparse.Values,thread._local,_thread._local 286 | 287 | # List of members which are set dynamically and missed by pylint inference 288 | # system, and so shouldn't trigger E1101 when accessed. Python regular 289 | # expressions are accepted. 290 | generated-members=query 291 | 292 | # List of decorators that produce context managers, such as 293 | # contextlib.contextmanager. Add to this list to register other decorators that 294 | # produce valid context managers. 295 | contextmanager-decorators=contextlib.contextmanager 296 | 297 | 298 | [VARIABLES] 299 | 300 | # Tells whether we should check for unused import in __init__ files. 301 | init-import=no 302 | 303 | # A regular expression matching the name of dummy variables (i.e. expectedly 304 | # not used). 305 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 306 | 307 | # List of additional names supposed to be defined in builtins. Remember that 308 | # you should avoid to define new builtins when possible. 309 | additional-builtins= 310 | 311 | # List of strings which can identify a callback function by name. A callback 312 | # name must start or end with one of those strings. 313 | callbacks=cb_,_cb 314 | 315 | # List of qualified module names which can have objects that can redefine 316 | # builtins. 317 | redefining-builtins-modules=six.moves,future.builtins 318 | 319 | 320 | [CLASSES] 321 | 322 | # List of method names used to declare (i.e. assign) instance attributes. 323 | defining-attr-methods=__init__,__new__,setUp 324 | 325 | # List of valid names for the first argument in a class method. 326 | valid-classmethod-first-arg=cls 327 | 328 | # List of valid names for the first argument in a metaclass class method. 329 | valid-metaclass-classmethod-first-arg=mcs 330 | 331 | # List of member names, which should be excluded from the protected access 332 | # warning. 333 | exclude-protected=_asdict,_fields,_replace,_source,_make 334 | 335 | 336 | [DESIGN] 337 | 338 | # Maximum number of arguments for function / method 339 | max-args=5 340 | 341 | # Argument names that match this expression will be ignored. Default to name 342 | # with leading underscore 343 | ignored-argument-names=_.* 344 | 345 | # Maximum number of locals for function / method body 346 | max-locals=15 347 | 348 | # Maximum number of return / yield for function / method body 349 | max-returns=6 350 | 351 | # Maximum number of branch for function / method body 352 | max-branches=12 353 | 354 | # Maximum number of statements in function / method body 355 | max-statements=50 356 | 357 | # Maximum number of parents for a class (see R0901). 358 | max-parents=7 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Minimum number of public methods for a class (see R0903). 364 | min-public-methods=2 365 | 366 | # Maximum number of public methods for a class (see R0904). 367 | max-public-methods=20 368 | 369 | # Maximum number of boolean expressions in a if statement 370 | max-bool-expr=5 371 | 372 | 373 | [IMPORTS] 374 | 375 | # Deprecated modules which should not be used, separated by a comma 376 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 377 | 378 | # Create a graph of every (i.e. internal and external) dependencies in the 379 | # given file (report RP0402 must not be disabled) 380 | import-graph= 381 | 382 | # Create a graph of external dependencies in the given file (report RP0402 must 383 | # not be disabled) 384 | ext-import-graph= 385 | 386 | # Create a graph of internal dependencies in the given file (report RP0402 must 387 | # not be disabled) 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Analyse import fallback blocks. This can be used to support both Python 2 and 398 | # 3 compatible code, which means that the block might have code that exists 399 | # only in one or another interpreter, leading to false positives when analysed. 400 | analyse-fallback-blocks=no 401 | 402 | 403 | [EXCEPTIONS] 404 | 405 | # Exceptions that will emit a warning when being caught. Defaults to 406 | # "Exception" 407 | overgeneral-exceptions=Exception 408 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | env: 3 | - PYTHONPATH=`pwd` 4 | python: 5 | - "2.7" 6 | - "3.5" 7 | - "3.6" 8 | - "3.6-dev" # 3.6 development branch 9 | - "3.7-dev" # 3.7 development branch 10 | - "nightly" # currently points to 3.7-dev 11 | # command to install dependencies 12 | install: 13 | - "pip install pytest" 14 | - "pip install -r requirements.txt" 15 | # command to run tests 16 | script: python -m pytest section3/video_code/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/schoolofcode-me/testing-python-apps.svg?branch=master)](https://travis-ci.org/schoolofcode-me/testing-python-apps) 2 | 3 | # Testing Python Apps 4 | 5 | This repository contains the code covered by each section of my latest course, 'Testing Python Apps'. 6 | 7 | It builds on another course, 'REST APIs with Flask and Python', to discuss testing at each level in order to build resilient applications. 8 | 9 | Course coming soon! -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-JWT 4 | Flask-SQLAlchemy 5 | psycopg2 6 | behave==1.2.5 7 | beautifulsoup4==4.5.3 8 | selenium==3.4.1 -------------------------------------------------------------------------------- /section10/README.md: -------------------------------------------------------------------------------- 1 | # Section 10: Acceptance testing 2 | 3 | On Windows you can still use the PyCharm Multirun plugin. 4 | 5 | Alternatively, PyCharm CE comes with a "Compound" run configuration which you can use to run two other run configurations sequentially. 6 | -------------------------------------------------------------------------------- /section10/code/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for 2 | 3 | app = Flask(__name__) 4 | 5 | posts = [] 6 | 7 | 8 | @app.route('/') 9 | def homepage(): 10 | return render_template('home.html') 11 | 12 | 13 | @app.route('/blog') 14 | def blog_page(): 15 | return render_template('blog.html', posts=posts) 16 | 17 | 18 | @app.route('/post', methods=['GET', 'POST']) 19 | def add_post(): 20 | if request.method == 'POST': 21 | title = request.form['title'] 22 | content = request.form['content'] 23 | global posts 24 | 25 | posts.append({ 26 | 'title': title, 27 | 'content': content 28 | }) 29 | 30 | return redirect(url_for('blog_page')) 31 | return render_template('new_post.html') 32 | 33 | 34 | @app.route('/post/') 35 | def see_post(title): 36 | global posts 37 | 38 | for post in posts: 39 | if post['title'] == title: 40 | return render_template('post.html', post=post) 41 | 42 | return render_template('post.html', post=None) 43 | 44 | 45 | if __name__ == '__main__': 46 | app.run() 47 | -------------------------------------------------------------------------------- /section10/code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | uwsgi 3 | behave==1.2.5 4 | beautifulsoup4==4.5.3 5 | selenium==3.4.1 -------------------------------------------------------------------------------- /section10/code/templates/blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 |

This is the blog page

15 | 16 | Go to home 17 | Create post 18 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /section10/code/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

This is the homepage

8 | 9 | Go to blog 10 | 11 | -------------------------------------------------------------------------------- /section10/code/templates/new_post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Create post

8 | 9 | Back to blog 10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /section10/code/templates/post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% if post %} 8 |

{{ post['title'] }}

9 | 10 |

{{ post['content'] }}

11 | {% else %} 12 |

Post not found!

13 | {% endif %} 14 | 15 | -------------------------------------------------------------------------------- /section10/code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/code/tests/__init__.py -------------------------------------------------------------------------------- /section10/code/tests/acceptance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/code/tests/acceptance/__init__.py -------------------------------------------------------------------------------- /section10/code/tests/acceptance/content.feature: -------------------------------------------------------------------------------- 1 | Feature: Test that pages have correct content 2 | Scenario: Blog page has a correct title 3 | Given I am on the blog page 4 | Then There is a title shown on the page 5 | And The title tag has content "This is the blog page" 6 | 7 | Scenario: Homepage has a correct title 8 | Given I am on the homepage 9 | Then There is a title shown on the page 10 | And The title tag has content "This is the homepage" 11 | 12 | Scenario: Blog page loads the posts 13 | Given I am on the blog page 14 | And I wait for the posts to load 15 | Then I can see there is a posts section on the page 16 | 17 | Scenario: User can create new posts 18 | Given I am on the new post page 19 | When I enter "Test Post" in the "title" field 20 | And I enter "Test Content" in the "content" field 21 | And I press the submit button 22 | Then I am on the blog page 23 | Given I wait for the posts to load 24 | Then I can see there is a post with title "Test Post" in the posts section -------------------------------------------------------------------------------- /section10/code/tests/acceptance/locators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/code/tests/acceptance/locators/__init__.py -------------------------------------------------------------------------------- /section10/code/tests/acceptance/locators/base_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | TITLE = By.TAG_NAME, 'h1' 6 | NAV_LINKS = By.CLASS_NAME, 'nav-link' 7 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/locators/blog_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BlogPageLocators: 5 | ADD_POST_LINK = By.ID, 'add-post-link' 6 | POSTS_SECTION = By.ID, 'posts' 7 | POST = By.CLASS_NAME, 'post-link' 8 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/locators/home_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class HomePageLocators: 5 | NAVIGATION_LINK = By.ID, 'blog-link' 6 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/locators/new_post_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class NewPostPageLocators: 5 | NEW_POST_FORM = By.ID, 'post-form' 6 | TITLE_FIELD = By.ID, 'title' 7 | CONTENT_FIELD = By.ID, 'content' 8 | SUBMIT_BUTTON = By.ID, 'create-post' 9 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/navigation.feature: -------------------------------------------------------------------------------- 1 | Feature: Test navigation between pages 2 | We can have a longer description 3 | That can span a few lines 4 | 5 | Scenario: Homepage can go to Blog 6 | Given I am on the homepage 7 | When I click on the "Go to blog" link 8 | Then I am on the blog page 9 | 10 | Scenario: Blog can go to Homepage 11 | Given I am on the blog page 12 | When I click on the "Go to home" link 13 | Then I am on the homepage -------------------------------------------------------------------------------- /section10/code/tests/acceptance/page_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/code/tests/acceptance/page_model/__init__.py -------------------------------------------------------------------------------- /section10/code/tests/acceptance/page_model/base_page.py: -------------------------------------------------------------------------------- 1 | from tests.acceptance.locators.base_page import BasePageLocators 2 | 3 | 4 | class BasePage: 5 | def __init__(self, driver): 6 | self.driver = driver 7 | 8 | @property 9 | def url(self): 10 | return 'http://127.0.0.1:5000' 11 | 12 | @property 13 | def title(self): 14 | return self.driver.find_element(*BasePageLocators.TITLE) 15 | 16 | @property 17 | def navigation(self): 18 | return self.driver.find_elements(*BasePageLocators.NAV_LINKS) 19 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/page_model/blog_page.py: -------------------------------------------------------------------------------- 1 | from tests.acceptance.locators.blog_page import BlogPageLocators 2 | from tests.acceptance.page_model.base_page import BasePage 3 | 4 | 5 | class BlogPage(BasePage): 6 | @property 7 | def url(self): 8 | return super(BlogPage, self).url + '/blog' 9 | 10 | @property 11 | def posts_section(self): 12 | return self.driver.find_element(*BlogPageLocators.POSTS_SECTION) 13 | 14 | @property 15 | def posts(self): 16 | return self.driver.find_elements(*BlogPageLocators.POST) 17 | 18 | @property 19 | def add_post_link(self): 20 | return self.driver.find_element(*BlogPageLocators.ADD_POST_LINK) 21 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/page_model/home_page.py: -------------------------------------------------------------------------------- 1 | from tests.acceptance.locators.home_page import HomePageLocators 2 | from tests.acceptance.page_model.base_page import BasePage 3 | 4 | 5 | class HomePage(BasePage): 6 | @property 7 | def url(self): 8 | return super(HomePage, self).url + '/' 9 | 10 | @property 11 | def blog_link(self): 12 | return self.driver.find_element(*HomePageLocators.NAVIGATION_LINK) 13 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/page_model/new_post_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | from tests.acceptance.locators.new_post_page import NewPostPageLocators 4 | from tests.acceptance.page_model.base_page import BasePage 5 | 6 | 7 | class NewPostPage(BasePage): 8 | @property 9 | def url(self): 10 | return super(NewPostPage, self).url + '/post' 11 | 12 | @property 13 | def form(self): 14 | return self.driver.find_element(*NewPostPageLocators.NEW_POST_FORM) 15 | 16 | @property 17 | def submit_button(self): 18 | return self.driver.find_element(*NewPostPageLocators.SUBMIT_BUTTON) 19 | 20 | def form_field(self, name): 21 | return self.form.find_element(By.NAME, name) 22 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/steps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/code/tests/acceptance/steps/__init__.py -------------------------------------------------------------------------------- /section10/code/tests/acceptance/steps/content.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | 3 | from tests.acceptance.page_model.base_page import BasePage 4 | from tests.acceptance.page_model.blog_page import BlogPage 5 | 6 | use_step_matcher('re') 7 | 8 | 9 | @then('There is a title shown on the page') 10 | def step_impl(context): 11 | page = BasePage(context.driver) 12 | assert page.title.is_displayed() 13 | 14 | 15 | @step('The title tag has content "(.*)"') 16 | def step_impl(context, content): 17 | page = BasePage(context.driver) 18 | assert page.title.text == content 19 | 20 | 21 | @then('I can see there is a posts section on the page') 22 | def step_impl(context): 23 | page = BlogPage(context.driver) 24 | 25 | assert page.posts_section.is_displayed() 26 | 27 | 28 | @then('I can see there is a post with title "(.*)" in the posts section') 29 | def step_impl(context, title): 30 | page = BlogPage(context.driver) 31 | posts_with_title = [post for post in page.posts if post.text == title] 32 | 33 | assert len(posts_with_title) > 0 34 | assert all([post.is_displayed() for post in posts_with_title]) 35 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/steps/interactions.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | 3 | from tests.acceptance.page_model.base_page import BasePage 4 | from tests.acceptance.page_model.new_post_page import NewPostPage 5 | 6 | use_step_matcher('re') 7 | 8 | 9 | @when('I click on the "(.*)" link') 10 | def step_impl(context, link_text): 11 | page = BasePage(context.driver) 12 | links = page.navigation 13 | 14 | matching_links = [l for l in links if l.text == link_text] 15 | 16 | if len(matching_links) > 0: 17 | matching_links[0].click() 18 | else: 19 | raise RuntimeError() 20 | 21 | 22 | @when('I enter "(.*)" in the "(.*)" field') 23 | def step_impl(context, content, field_name): 24 | page = NewPostPage(context.driver) 25 | page.form_field(field_name).send_keys(content) 26 | 27 | 28 | @when('I press the submit button') 29 | def step_impl(context): 30 | page = NewPostPage(context.driver) 31 | page.submit_button.click() 32 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/steps/navigation.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | from selenium import webdriver 3 | 4 | from tests.acceptance.page_model.blog_page import BlogPage 5 | from tests.acceptance.page_model.home_page import HomePage 6 | from tests.acceptance.page_model.new_post_page import NewPostPage 7 | 8 | use_step_matcher('re') 9 | 10 | 11 | @given('I am on the homepage') 12 | def step_impl(context): 13 | context.driver = webdriver.Chrome() 14 | page = HomePage(context.driver) 15 | context.driver.get(page.url) 16 | 17 | 18 | @given('I am on the blog page') 19 | def step_impl(context): 20 | context.driver = webdriver.Chrome() 21 | page = BlogPage(context.driver) 22 | context.driver.get(page.url) 23 | 24 | 25 | @given('I am on the new post page') 26 | def step_impl(context): 27 | context.driver = webdriver.Chrome() 28 | page = NewPostPage(context.driver) 29 | context.driver.get(page.url) 30 | 31 | 32 | @then('I am on the blog page') 33 | def step_impl(context): 34 | expected_url = BlogPage(context.driver).url 35 | assert context.driver.current_url == expected_url 36 | 37 | 38 | @then('I am on the homepage') 39 | def step_impl(context): 40 | expected_url = HomePage(context.driver).url 41 | assert context.driver.current_url == expected_url 42 | -------------------------------------------------------------------------------- /section10/code/tests/acceptance/steps/waits.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | from selenium.webdriver.support import expected_conditions 3 | from selenium.webdriver.support.wait import WebDriverWait 4 | 5 | from tests.acceptance.locators.blog_page import BlogPageLocators 6 | 7 | use_step_matcher('re') 8 | 9 | 10 | @given('I wait for the posts to load') 11 | def step_impl(context): 12 | WebDriverWait(context.driver, 5).until( 13 | expected_conditions.visibility_of_element_located(BlogPageLocators.POSTS_SECTION) 14 | ) 15 | -------------------------------------------------------------------------------- /section10/video_code/README.md: -------------------------------------------------------------------------------- 1 | # Set up 2 | 3 | We'll need a few things to install for this section: 4 | 5 | - https://sites.google.com/a/chromium.org/chromedriver/downloads 6 | - behave (http://pythonhosted.org/behave/) 7 | - selenium (http://selenium-python.readthedocs.io/installation.html) 8 | 9 | 10 | ## Running the tests 11 | 12 | To run the tests, you'll need to do this in a terminal (but remember to have the Flask app running!): 13 | 14 | ```bash 15 | source venv/bin/activate 16 | cd section6/video_code/ 17 | python -m behave tests/acceptance 18 | ``` 19 | 20 | If you want to run the tests in PyCharm, you'll need to create appropriate configurations. We cover this in the course! -------------------------------------------------------------------------------- /section10/video_code/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for 2 | 3 | app = Flask(__name__) 4 | 5 | posts = [] 6 | 7 | 8 | @app.route('/') 9 | def homepage(): 10 | return render_template('home.html') 11 | 12 | 13 | @app.route('/blog') 14 | def blog_page(): 15 | return render_template('blog.html', posts=posts) 16 | 17 | 18 | @app.route('/post', methods=['GET', 'POST']) 19 | def add_post(): 20 | if request.method == 'POST': 21 | title = request.form['title'] 22 | content = request.form['content'] 23 | global posts 24 | 25 | posts.append({ 26 | 'title': title, 27 | 'content': content 28 | }) 29 | 30 | return redirect(url_for('blog_page')) 31 | return render_template('new_post.html') 32 | 33 | 34 | @app.route('/post/') 35 | def see_post(title): 36 | global posts 37 | 38 | for post in posts: 39 | if post['title'] == title: 40 | return render_template('post.html', post=post) 41 | 42 | return render_template('post.html', post=None) 43 | 44 | 45 | if __name__ == '__main__': 46 | app.run() 47 | -------------------------------------------------------------------------------- /section10/video_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | uwsgi 3 | behave==1.2.5 4 | beautifulsoup4==4.5.3 5 | selenium==3.4.1 -------------------------------------------------------------------------------- /section10/video_code/templates/blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 |

This is the blog page

15 | 16 | Go to home 17 | Create post 18 | 19 | {% if posts|length > 0 %} 20 | 25 | {% endif %} 26 | 27 | -------------------------------------------------------------------------------- /section10/video_code/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

This is the homepage

8 | 9 | Go to blog 10 | 11 | -------------------------------------------------------------------------------- /section10/video_code/templates/new_post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Create post

8 | 9 | Back to blog 10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /section10/video_code/templates/post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% if post %} 8 |

{{ post['title'] }}

9 | 10 |

{{ post['content'] }}

11 | {% else %} 12 |

Post not found!

13 | {% endif %} 14 | 15 | -------------------------------------------------------------------------------- /section10/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/video_code/tests/acceptance/__init__.py -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/content.feature: -------------------------------------------------------------------------------- 1 | Feature: Test that pages have correct content 2 | Scenario: Blog page has an appropriate title 3 | Given I am on the blog page 4 | Then There is a title shown on the page 5 | And The title tag has content "This is the blog page" 6 | 7 | Scenario: Homepage has an appropriate title 8 | Given I am on the homepage 9 | Then There is a title shown on the page 10 | And The title tag has content "This is the homepage" 11 | 12 | Scenario: Blog page loads the posts 13 | Given I am on the blog page 14 | And I wait for the posts to load 15 | Then I can see there is a posts section on the page 16 | 17 | Scenario: User can create new posts 18 | Given I am on the new post page 19 | When I enter "Test Post" in the "title" field 20 | And I enter "Test Content" in the "content" field 21 | And I press the submit button 22 | Then I am on the blog page 23 | Given I wait for the posts to load 24 | Then I can see there is a post with title "Test Post" in the posts section -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/navigation.feature: -------------------------------------------------------------------------------- 1 | Feature: Test navigation between pages 2 | Make sure homepage can go to blog, and 3 | blog can go to homepage 4 | 5 | Scenario: Homepage can go to Blog 6 | Given I am on the homepage 7 | When I click on the link with id "blog-link" 8 | Then I am on the blog page 9 | 10 | Scenario: Blog page can go to homepage 11 | Given I am on the blog page 12 | When I click on the link with id "home-link" 13 | Then I am on the homepage 14 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/page_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/video_code/tests/acceptance/page_model/__init__.py -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/page_model/base_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | TITLE = By.TAG_NAME, 'h1' 6 | NAV_LINKS = By.CLASS_NAME, 'nav-link' 7 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/page_model/blog_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BlogPageLocators: 5 | POSTS_SECTION = By.ID, 'posts' 6 | POST = By.CLASS_NAME, 'post-link' 7 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/page_model/home_page.py: -------------------------------------------------------------------------------- 1 | class HomePageLocators: 2 | pass 3 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/page_model/new_post_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class NewPostPageLocators: 5 | NEW_POST_FORM = By.ID, 'post-form' 6 | TITLE_FIELD = By.ID, 'title' 7 | CONTENT_FIELD = By.ID, 'content' 8 | SUBMIT_BUTTON = By.ID, 'create-post' 9 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/page_model/post_page.py: -------------------------------------------------------------------------------- 1 | class PostPageLocators: 2 | pass 3 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/video_code/tests/acceptance/pages/__init__.py -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/pages/base_page.py: -------------------------------------------------------------------------------- 1 | from section10.video_code.tests.acceptance.page_model.base_page import BasePageLocators 2 | 3 | 4 | class BasePage: 5 | 6 | def __init__(self, driver): 7 | self.driver = driver 8 | 9 | @property 10 | def url(self): 11 | return 'http://127.0.0.1' 12 | 13 | def find_element(self, by, value): 14 | return self.driver.find_element(by, value) 15 | 16 | @property 17 | def title(self): 18 | return self.find_element(*BasePageLocators.TITLE) 19 | 20 | @property 21 | def navigation(self): 22 | return self.find_elements(*BasePageLocators.NAV_LINKS) 23 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/pages/blog_page.py: -------------------------------------------------------------------------------- 1 | from section10.video_code.tests.acceptance.page_model.blog_page import BlogPageLocators 2 | from section10.video_code.tests.acceptance.pages.base_page import BasePage 3 | 4 | 5 | class BlogPage(BasePage): 6 | @property 7 | def url(self): 8 | return super(BlogPage, self).url + '/blog' 9 | 10 | @property 11 | def posts_section(self): 12 | return self.driver.find_element(*BlogPageLocators.POSTS_SECTION) 13 | 14 | @property 15 | def posts(self): 16 | return self.driver.find_elements(*BlogPageLocators.POST) 17 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/pages/home_page.py: -------------------------------------------------------------------------------- 1 | from section10.video_code.tests.acceptance.pages.base_page import BasePage 2 | 3 | 4 | class HomePage(BasePage): 5 | @property 6 | def url(self): 7 | return super(HomePage, self).url + '/' 8 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/pages/new_post_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | from tests.acceptance.locators.new_post_page import NewPostPageLocators 4 | from tests.acceptance.page_model.base_page import BasePage 5 | 6 | 7 | class NewPostPage(BasePage): 8 | @property 9 | def url(self): 10 | return super(NewPostPage, self).url + '/post' 11 | 12 | @property 13 | def form(self): 14 | return self.driver.find_element(*NewPostPageLocators.NEW_POST_FORM) 15 | 16 | @property 17 | def submit_button(self): 18 | return self.driver.find_element(*NewPostPageLocators.SUBMIT_BUTTON) 19 | 20 | def form_field(self, name): 21 | return self.form.find_element(By.NAME, name) 22 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/pages/post_page.py: -------------------------------------------------------------------------------- 1 | from section10.video_code.tests.acceptance.pages.base_page import BasePage 2 | 3 | 4 | class PostPage(BasePage): 5 | 6 | def __init__(self, driver, post_title): 7 | super(PostPage, self).__init__(driver) 8 | self.post_title = post_title 9 | 10 | @property 11 | def url(self): 12 | return super(PostPage, self).url + '/post/' + self.post_title 13 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/steps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section10/video_code/tests/acceptance/steps/__init__.py -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/steps/content.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | 3 | from section10.video_code.tests.acceptance.pages.base_page import BasePage 4 | from section10.video_code.tests.acceptance.pages.blog_page import BlogPage 5 | 6 | use_step_matcher('re') 7 | 8 | 9 | @then('There is a title shown on the page') 10 | def step_impl(context): 11 | page = BasePage(context.browser) 12 | tag = page.title 13 | assert tag is not None 14 | assert tag.is_displayed() 15 | 16 | 17 | @then('The title tag has content "(.*)"') 18 | def step_impl(context, content): 19 | page = BasePage(context.browser) 20 | tag = page.title 21 | assert tag.text == content 22 | 23 | 24 | @then('I can see there is a posts section on the page') 25 | def step_impl(context): 26 | page = BlogPage(context.browser) 27 | tag = page.posts_section 28 | 29 | assert tag.is_displayed() 30 | 31 | 32 | @then('I can see there is a post with title "(.*)" in the posts section') 33 | def step_impl(context, title): 34 | page = BlogPage(context.browser) 35 | tags = page.posts 36 | 37 | posts_with_title = [post for post in tags if post.text == title] 38 | 39 | assert len(posts_with_title) > 0 40 | # assert posts_with_title[0].is_displayed() 41 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/steps/interaction.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | 3 | from tests.acceptance.page_model.base_page import BasePage 4 | from tests.acceptance.page_model.new_post_page import NewPostPage 5 | 6 | use_step_matcher('re') 7 | 8 | 9 | @when('I click on the "(.*)" link') 10 | def step_impl(context, link_text): 11 | page = BasePage(context.driver) 12 | links = page.navigation 13 | 14 | matching_links = [l for l in links if l.text == link_text] 15 | 16 | if len(matching_links) > 0: 17 | matching_links[0].click() 18 | else: 19 | raise RuntimeError() 20 | 21 | 22 | @when('I enter "(.*)" in the "(.*)" field') 23 | def step_impl(context, content, field_name): 24 | page = NewPostPage(context.driver) 25 | page.form_field(field_name).send_keys(content) 26 | 27 | 28 | @when('I press the submit button') 29 | def step_impl(context): 30 | page = NewPostPage(context.driver) 31 | page.submit_button.click() -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/steps/navigation.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | from selenium import webdriver 3 | 4 | from section10.video_code.tests.acceptance.pages.base_page import BasePage 5 | from section10.video_code.tests.acceptance.pages.blog_page import BlogPage 6 | from section10.video_code.tests.acceptance.pages.new_post_page import NewPostPage 7 | 8 | use_step_matcher('re') 9 | 10 | 11 | @given('I am on the homepage') 12 | def step_impl(context): 13 | context.browser = webdriver.Chrome() 14 | page = BasePage(context.browser) 15 | context.browser.get(page.url) 16 | 17 | 18 | @given('I am on the blog page') 19 | def step_impl(context): 20 | context.browser = webdriver.Chrome() 21 | page = BlogPage(context.browser) 22 | context.browser.get(page.url) 23 | 24 | 25 | @given('I am on the new post page') 26 | def step_impl(context): 27 | context.browser = webdriver.Chrome() 28 | page = NewPostPage(context.browser) 29 | context.browser.get(page.url) 30 | 31 | 32 | @then('I am on the homepage') 33 | def step_impl(context): 34 | page = BasePage(context.browser) 35 | assert context.browser.current_url == page.url 36 | 37 | 38 | @then('I am on the blog page') 39 | def step_impl(context): 40 | page = BlogPage(context.browser) 41 | assert context.browser.current_url == page.url 42 | -------------------------------------------------------------------------------- /section10/video_code/tests/acceptance/steps/waits.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | from selenium.webdriver.support.ui import WebDriverWait 3 | from selenium.webdriver.support import expected_conditions 4 | 5 | from section10.video_code.tests.acceptance.page_model.blog_page import BlogPageLocators 6 | 7 | use_step_matcher('re') 8 | 9 | 10 | @given('I wait for the posts to load') 11 | def step_impl(context): 12 | try: 13 | WebDriverWait(context.browser, 5).until( 14 | expected_conditions.visibility_of_element_located(BlogPageLocators.POSTS_SECTION) 15 | ) 16 | except: 17 | raise Exception() 18 | -------------------------------------------------------------------------------- /section3/video_code/app.py: -------------------------------------------------------------------------------- 1 | from blog import Blog 2 | 3 | 4 | MENU_PROMPT = '\nEnter "c" to create a blog, "l" to list them, "r" to read one, "p" to write a post, or "q" to quit: ' 5 | POST_TEMPLATE = """ 6 | --- {} --- 7 | 8 | {} 9 | 10 | """ 11 | 12 | blogs = dict() 13 | 14 | 15 | def menu(): 16 | print_blogs() 17 | selection = input(MENU_PROMPT) 18 | while selection != 'q': 19 | if selection == 'c': 20 | ask_create_blog() 21 | elif selection == 'l': 22 | print_blogs() 23 | elif selection == 'r': 24 | ask_read_blog() 25 | elif selection == 'p': 26 | ask_create_post() 27 | selection = input(MENU_PROMPT) 28 | 29 | 30 | def print_blogs(): 31 | for key, blog in blogs.items(): 32 | print('- {}'.format(blog)) 33 | 34 | 35 | def ask_create_blog(): 36 | title = input("Enter your blog title: ") 37 | author = input("Enter your name: ") 38 | 39 | blogs[title] = Blog(title, author) 40 | 41 | 42 | def ask_read_blog(): 43 | title = input("Enter the blog title you want to read: ") 44 | 45 | print_posts(blogs[title]) 46 | 47 | 48 | def print_posts(blog): 49 | for post in blog.posts: 50 | print_post(post) 51 | 52 | 53 | def print_post(post): 54 | print(POST_TEMPLATE.format(post.title, post.content)) 55 | 56 | 57 | def ask_create_post(): 58 | blog = input("Enter the blog title you want to create a post in: ") 59 | title = input("Enter your post title: ") 60 | content = input("Enter your post content: ") 61 | 62 | blogs[blog].create_post(title, content) 63 | 64 | 65 | if __name__ == '__main__': 66 | menu() 67 | -------------------------------------------------------------------------------- /section3/video_code/blog.py: -------------------------------------------------------------------------------- 1 | from post import Post 2 | 3 | 4 | class Blog: 5 | def __init__(self, title, author): 6 | self.title = title 7 | self.author = author 8 | self.posts = [] 9 | 10 | def __repr__(self): 11 | return '{} by {} ({} posts)'.format(self.title, self.author, len(self.posts)) 12 | 13 | def create_post(self, title, content): 14 | self.posts.append(Post(title, content)) 15 | 16 | def json(self): 17 | return { 18 | 'title': self.title, 19 | 'author': self.author, 20 | 'posts': [post.json() for post in self.posts], 21 | } 22 | -------------------------------------------------------------------------------- /section3/video_code/post.py: -------------------------------------------------------------------------------- 1 | class Post: 2 | def __init__(self, title, content): 3 | self.title = title 4 | self.content = content 5 | 6 | def json(self): 7 | return { 8 | 'title': self.title, 9 | 'content': self.content, 10 | } 11 | -------------------------------------------------------------------------------- /section3/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section3/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section3/video_code/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section3/video_code/tests/integration/__init__.py -------------------------------------------------------------------------------- /section3/video_code/tests/integration/blog_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from blog import Blog 3 | 4 | 5 | class BlogTest(TestCase): 6 | def test_create_post_in_blog(self): 7 | b = Blog('Test', 'Test author') 8 | b.create_post('Test Post', 'Test Content') 9 | 10 | self.assertEqual(b.posts[0].title, 'Test Post') 11 | self.assertEqual(b.posts[0].content, 'Test Content') 12 | 13 | def test_blog_repr(self): 14 | b = Blog('Test', 'Test author') 15 | 16 | self.assertEqual(str(b), 'Test by Test author (0 posts)') 17 | 18 | def test_json_with_posts(self): 19 | b = Blog('Test', 'Test author') 20 | b.create_post('Test Post', 'Test Content') 21 | 22 | self.assertDictEqual(b.json(), { 23 | 'title': b.title, 24 | 'author': b.author, 25 | 'posts': [{'title': 'Test Post', 'content': 'Test Content'}], 26 | }) 27 | -------------------------------------------------------------------------------- /section3/video_code/tests/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section3/video_code/tests/system/__init__.py -------------------------------------------------------------------------------- /section3/video_code/tests/system/app_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | try: 3 | from unittest.mock import patch 4 | except ImportError: 5 | from mock import patch 6 | 7 | import app 8 | from blog import Blog 9 | from post import Post 10 | 11 | 12 | class AppTest(TestCase): 13 | def setUp(self): 14 | blog = Blog('Test', 'Test Author') 15 | app.blogs = {'Test': blog} 16 | 17 | def test_menu_prints_blogs(self): 18 | with patch('builtins.print') as mocked_print: 19 | with patch('builtins.input', return_value='q'): 20 | app.menu() 21 | 22 | mocked_print.assert_called_with('- Test by Test Author (0 posts)') 23 | 24 | def test_menu_prints_prompt(self): 25 | with patch('builtins.input', return_value='q') as mocked_input: 26 | app.menu() 27 | 28 | mocked_input.assert_called_with(app.MENU_PROMPT) 29 | 30 | def test_menu_calls_create_blog(self): 31 | with patch('builtins.input') as mocked_input: 32 | mocked_input.side_effect = ('c', 'Test Two', 'Test Author Two', 'q') 33 | app.menu() 34 | 35 | self.assertIsNotNone(app.blogs['Test Two']) 36 | 37 | def test_menu_calls_print_blogs(self): 38 | with patch('builtins.input') as mocked_input: 39 | with patch('app.print_blogs') as mocked_print_blogs: 40 | mocked_input.side_effect = ('l', 'q') 41 | app.menu() 42 | 43 | mocked_print_blogs.assert_called() 44 | 45 | def test_menu_calls_ask_read_blogs(self): 46 | with patch('builtins.input') as mocked_input: 47 | with patch('app.ask_read_blog') as mocked_ask_read_blog: 48 | mocked_input.side_effect = ('r', 'Test', 'q') 49 | app.menu() 50 | 51 | mocked_ask_read_blog.assert_called() 52 | 53 | def test_menu_calls_ask_create_post(self): 54 | with patch('builtins.input') as mocked_input: 55 | with patch('app.ask_create_post') as mocked_ask_create_post: 56 | mocked_input.side_effect = ('p', 'Test', 'New Post', 'New Content', 'q') 57 | app.menu() 58 | 59 | mocked_ask_create_post.assert_called() 60 | 61 | def test_print_blogs(self): 62 | with patch('builtins.print') as mocked_print: 63 | app.print_blogs() 64 | mocked_print.assert_called_with('- Test by Test Author (0 posts)') 65 | 66 | def test_ask_create_blog(self): 67 | with patch('builtins.input') as mocked_input: 68 | mocked_input.side_effect = ('Test', 'Author') 69 | 70 | app.ask_create_blog() 71 | 72 | self.assertIsNotNone(app.blogs.get('Test')) 73 | self.assertEqual(app.blogs.get('Test').title, 'Test') 74 | self.assertEqual(app.blogs.get('Test').author, 'Author') 75 | 76 | def test_ask_read_blog(self): 77 | with patch('builtins.input', return_value='Test'): 78 | with patch('app.print_posts') as mocked_print_posts: 79 | app.ask_read_blog() 80 | 81 | mocked_print_posts.assert_called_with(app.blogs['Test']) 82 | 83 | def test_print_posts(self): 84 | blog = app.blogs['Test'] 85 | blog.create_post('Post title', 'Post content') 86 | 87 | with patch('app.print_post') as mocked_print_post: 88 | app.print_posts(blog) 89 | 90 | mocked_print_post.assert_called_with(blog.posts[0]) 91 | 92 | def test_print_post(self): 93 | post = Post('Post title', 'Post content') 94 | expected_print = """ 95 | --- Post title --- 96 | 97 | Post content 98 | 99 | """ 100 | 101 | with patch('builtins.print') as mocked_print: 102 | app.print_post(post) 103 | 104 | mocked_print.assert_called_with(expected_print) 105 | 106 | def test_ask_create_post(self): 107 | blog = app.blogs['Test'] 108 | with patch('builtins.input') as mocked_input: 109 | mocked_input.side_effect = ('Test', 'Test Title', 'Test Content') 110 | 111 | app.ask_create_post() 112 | 113 | self.assertEqual(blog.posts[0].title, 'Test Title') 114 | self.assertEqual(blog.posts[0].content, 'Test Content') 115 | -------------------------------------------------------------------------------- /section3/video_code/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section3/video_code/tests/unit/__init__.py -------------------------------------------------------------------------------- /section3/video_code/tests/unit/blog_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from blog import Blog 3 | 4 | 5 | class BlogTest(TestCase): 6 | def test_create_blog(self): 7 | b = Blog('Test', 'Test author') 8 | 9 | self.assertEqual('Test', b.title) 10 | self.assertEqual('Test author', b.author) 11 | self.assertEqual(0, len(b.posts)) 12 | self.assertListEqual([], b.posts) 13 | 14 | def test_json_no_posts(self): 15 | b = Blog('Test', 'Test author') 16 | 17 | self.assertDictEqual(b.json(), { 18 | 'title': b.title, 19 | 'author': b.author, 20 | 'posts': [], 21 | }) -------------------------------------------------------------------------------- /section3/video_code/tests/unit/post_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from post import Post 3 | 4 | 5 | class PostTest(TestCase): 6 | def test_create_post(self): 7 | p = Post('Test', 'Test content') 8 | 9 | self.assertEqual('Test', p.title) 10 | self.assertEqual('Test content', p.content) 11 | 12 | def test_json(self): 13 | p = Post('Test', 'Test content') 14 | 15 | self.assertDictEqual({'title': p.title, 'content': p.content}, p.json()) 16 | -------------------------------------------------------------------------------- /section4/video_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section4/video_code/__init__.py -------------------------------------------------------------------------------- /section4/video_code/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | 3 | 4 | app = Flask(__name__) 5 | 6 | 7 | @app.route('/') 8 | def home(): 9 | return jsonify({'message': 'Hello, world!'}) 10 | 11 | 12 | if __name__ == '__main__': 13 | app.run() 14 | -------------------------------------------------------------------------------- /section4/video_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | -------------------------------------------------------------------------------- /section4/video_code/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello, world!

4 | 5 | -------------------------------------------------------------------------------- /section4/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section4/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section4/video_code/tests/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section4/video_code/tests/system/__init__.py -------------------------------------------------------------------------------- /section4/video_code/tests/system/base_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseTest 3 | 4 | This class should be the parent class to each system test. 5 | It gives each test a Flask test client that we can use. 6 | """ 7 | 8 | from unittest import TestCase 9 | from app import app 10 | 11 | 12 | class BaseTest(TestCase): 13 | def setUp(self): 14 | self.app.testing = True 15 | self.app = app.test_client 16 | -------------------------------------------------------------------------------- /section4/video_code/tests/system/test_home.py: -------------------------------------------------------------------------------- 1 | from tests.system.base_test import BaseTest 2 | import json 3 | 4 | 5 | class TestHome(BaseTest): 6 | def test_home(self): 7 | with self.app() as c: 8 | r = c.get('/') 9 | self.assertEqual(r.status_code, 200) 10 | self.assertEqual(json.loads(r.get_data()), {'message': 'Hello, world!'}) 11 | -------------------------------------------------------------------------------- /section5/starter_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/starter_code/__init__.py -------------------------------------------------------------------------------- /section5/starter_code/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask_restful import Api 5 | 6 | from resources.item import Item, ItemList 7 | 8 | app = Flask(__name__) 9 | 10 | app.config['DEBUG'] = True 11 | 12 | app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') 13 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 14 | api = Api(app) 15 | 16 | api.add_resource(Item, '/item/') 17 | 18 | if __name__ == '__main__': 19 | from db import db 20 | 21 | db.init_app(app) 22 | 23 | if app.config['DEBUG']: 24 | @app.before_first_request 25 | def create_tables(): 26 | db.create_all() 27 | 28 | app.run(port=5000) 29 | -------------------------------------------------------------------------------- /section5/starter_code/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /section5/starter_code/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/starter_code/models/__init__.py -------------------------------------------------------------------------------- /section5/starter_code/models/item.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class ItemModel(db.Model): 5 | __tablename__ = 'items' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | price = db.Column(db.Float(precision=2)) 10 | 11 | def __init__(self, name, price): 12 | self.name = name 13 | self.price = price 14 | 15 | def json(self): 16 | return {'name': self.name, 'price': self.price} 17 | 18 | @classmethod 19 | def find_by_name(cls, name): 20 | return cls.query.filter_by(name=name).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def delete_from_db(self): 27 | db.session.delete(self) 28 | db.session.commit() 29 | -------------------------------------------------------------------------------- /section5/starter_code/readme.md: -------------------------------------------------------------------------------- 1 | # Stores REST Api 2 | 3 | This is built with Flask, Flask-RESTful, Flask-JWT, and Flask-SQLAlchemy. 4 | 5 | To get started: 6 | 7 | - Create a virtualenv for this project 8 | - Install requirements using `pip install -r requirements.txt` 9 | 10 | When you've created the first test, you'll also need to create a correct runtime configuration in PyCharm. 11 | 12 | Create a sample unittest configuration, and choose: 13 | 14 | - `Path` as target, with your project's `/tests` folder. 15 | -------------------------------------------------------------------------------- /section5/starter_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-SQLAlchemy 4 | psycopg2 5 | -------------------------------------------------------------------------------- /section5/starter_code/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/starter_code/resources/__init__.py -------------------------------------------------------------------------------- /section5/starter_code/resources/item.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from models.item import ItemModel 3 | 4 | 5 | class Item(Resource): 6 | parser = reqparse.RequestParser() 7 | parser.add_argument('price', 8 | type=float, 9 | required=True, 10 | help="This field cannot be left blank!") 11 | 12 | def get(self, name): 13 | item = ItemModel.find_by_name(name) 14 | if item: 15 | return item.json() 16 | return {'message': 'Item not found'}, 404 17 | 18 | def post(self, name): 19 | if ItemModel.find_by_name(name): 20 | return {'message': "An item with name '{}' already exists.".format(name)}, 400 21 | 22 | data = Item.parser.parse_args() 23 | 24 | item = ItemModel(name, **data) 25 | 26 | try: 27 | item.save_to_db() 28 | except: 29 | return {"message": "An error occurred inserting the item."}, 500 30 | 31 | return item.json(), 201 32 | 33 | def delete(self, name): 34 | item = ItemModel.find_by_name(name) 35 | if item: 36 | item.delete_from_db() 37 | 38 | return {'message': 'Item deleted'} 39 | 40 | def put(self, name): 41 | data = Item.parser.parse_args() 42 | 43 | item = ItemModel.find_by_name(name) 44 | 45 | if item is None: 46 | item = ItemModel(name, **data) 47 | else: 48 | item.price = data['price'] 49 | 50 | item.save_to_db() 51 | 52 | return item.json() 53 | 54 | 55 | class ItemList(Resource): 56 | def get(self): 57 | return {'items': [x.json() for x in ItemModel.query.all()]} 58 | -------------------------------------------------------------------------------- /section5/starter_code/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | 4 | db.init_app(app) 5 | 6 | 7 | @app.before_first_request 8 | def create_tables(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /section5/starter_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/starter_code/tests/__init__.py -------------------------------------------------------------------------------- /section5/video_code/Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi.ini 2 | -------------------------------------------------------------------------------- /section5/video_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/__init__.py -------------------------------------------------------------------------------- /section5/video_code/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask_restful import Api 5 | 6 | from resources.item import Item 7 | 8 | app = Flask(__name__) 9 | 10 | app.config['DEBUG'] = True 11 | 12 | app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') 13 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 14 | api = Api(app) 15 | 16 | api.add_resource(Item, '/item/') 17 | 18 | if __name__ == '__main__': 19 | from db import db 20 | 21 | db.init_app(app) 22 | 23 | if app.config['DEBUG']: 24 | @app.before_first_request 25 | def create_tables(): 26 | db.create_all() 27 | 28 | app.run(port=5000) 29 | -------------------------------------------------------------------------------- /section5/video_code/db.py: -------------------------------------------------------------------------------- 1 | 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /section5/video_code/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/models/__init__.py -------------------------------------------------------------------------------- /section5/video_code/models/item.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class ItemModel(db.Model): 5 | __tablename__ = 'items' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | price = db.Column(db.Float(precision=2)) 10 | 11 | def __init__(self, name, price): 12 | self.name = name 13 | self.price = price 14 | 15 | def json(self): 16 | return {'name': self.name, 'price': self.price} 17 | 18 | @classmethod 19 | def find_by_name(cls, name): 20 | return cls.query.filter_by(name=name).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def delete_from_db(self): 27 | db.session.delete(self) 28 | db.session.commit() 29 | -------------------------------------------------------------------------------- /section5/video_code/readme.md: -------------------------------------------------------------------------------- 1 | # Stores REST Api 2 | 3 | This is built with Flask, Flask-RESTful, Flask-JWT, and Flask-SQLAlchemy. 4 | 5 | Deployed on Heroku. 6 | 7 | To get started: 8 | 9 | - Create a virtualenv for this project 10 | - Install requirements using `pip install -r requirements.txt` 11 | 12 | When you've created the first test, you'll also need to create a correct runtime configuration in PyCharm. 13 | 14 | Create a sample unittest configuration, and choose: 15 | 16 | - `Path` as as target, with your project's `/tests` folder. 17 | - `Pattern` being `_test.py` -------------------------------------------------------------------------------- /section5/video_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-JWT 4 | Flask-SQLAlchemy 5 | uwsgi 6 | psycopg2 7 | -------------------------------------------------------------------------------- /section5/video_code/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/resources/__init__.py -------------------------------------------------------------------------------- /section5/video_code/resources/item.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from models.item import ItemModel 3 | 4 | 5 | class Item(Resource): 6 | parser = reqparse.RequestParser() 7 | parser.add_argument('price', 8 | type=float, 9 | required=True, 10 | help="This field cannot be left blank!") 11 | 12 | def get(self, name): 13 | item = ItemModel.find_by_name(name) 14 | if item: 15 | return item.json() 16 | return {'message': 'Item not found'}, 404 17 | 18 | def post(self, name): 19 | if ItemModel.find_by_name(name): 20 | return {'message': "An item with name '{}' already exists.".format(name)}, 400 21 | 22 | data = Item.parser.parse_args() 23 | 24 | item = ItemModel(name, **data) 25 | 26 | try: 27 | item.save_to_db() 28 | except: 29 | return {"message": "An error occurred inserting the item."}, 500 30 | 31 | return item.json(), 201 32 | 33 | def delete(self, name): 34 | item = ItemModel.find_by_name(name) 35 | if item: 36 | item.delete_from_db() 37 | 38 | return {'message': 'Item deleted'} 39 | 40 | def put(self, name): 41 | data = Item.parser.parse_args() 42 | 43 | item = ItemModel.find_by_name(name) 44 | 45 | if item is None: 46 | item = ItemModel(name, **data) 47 | else: 48 | item.price = data['price'] 49 | 50 | item.save_to_db() 51 | 52 | return item.json() 53 | -------------------------------------------------------------------------------- /section5/video_code/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | 4 | db.init_app(app) 5 | 6 | 7 | @app.before_first_request 8 | def create_tables(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /section5/video_code/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.2 2 | -------------------------------------------------------------------------------- /section5/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section5/video_code/tests/base_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseTest 3 | 4 | This class should be the parent class to each non-unit test. 5 | It allows for instantiation of the database dynamically 6 | and makes sure that it is a new, blank database each time. 7 | """ 8 | 9 | from unittest import TestCase 10 | from app import app 11 | from db import db 12 | 13 | 14 | class BaseTest(TestCase): 15 | def setUp(self): 16 | # Make sure database exists 17 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' 18 | with app.app_context(): 19 | db.init_app(app) 20 | db.create_all() 21 | # Get a test client 22 | self.app = app.test_client() 23 | self.app_context = app.app_context 24 | 25 | def tearDown(self): 26 | # Database is blank 27 | with app.app_context(): 28 | db.session.remove() 29 | db.drop_all() 30 | -------------------------------------------------------------------------------- /section5/video_code/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/tests/integration/__init__.py -------------------------------------------------------------------------------- /section5/video_code/tests/integration/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/tests/integration/models/__init__.py -------------------------------------------------------------------------------- /section5/video_code/tests/integration/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class ItemTest(BaseTest): 6 | def test_crud(self): 7 | with self.app_context(): 8 | item = ItemModel('test', 19.99) 9 | 10 | self.assertIsNone(ItemModel.find_by_name('test'), 11 | "Found an item with name {}, but expected not to.".format(item.name)) 12 | 13 | item.save_to_db() 14 | 15 | self.assertIsNotNone(ItemModel.find_by_name('test')) 16 | 17 | item.delete_from_db() 18 | 19 | self.assertIsNone(ItemModel.find_by_name('test')) 20 | -------------------------------------------------------------------------------- /section5/video_code/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/tests/unit/__init__.py -------------------------------------------------------------------------------- /section5/video_code/tests/unit/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section5/video_code/tests/unit/models/__init__.py -------------------------------------------------------------------------------- /section5/video_code/tests/unit/models/item_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from models.item import ItemModel 4 | 5 | 6 | class ItemTest(TestCase): 7 | def test_create_item(self): 8 | item = ItemModel('test', 19.99) 9 | 10 | self.assertEqual(item.name, 'test', 11 | "The name of the item after creation does not equal the constructor argument.") 12 | self.assertEqual(item.price, 19.99, 13 | "The price of the item after creation does not equal the constructor argument.") 14 | 15 | def test_item_json(self): 16 | item = ItemModel('test', 19.99) 17 | expected = { 18 | 'name': 'test', 19 | 'price': 19.99 20 | } 21 | 22 | self.assertEqual( 23 | item.json(), 24 | expected, 25 | "The JSON export of the item is incorrect. Received {}, expected {}.".format(item.json(), expected)) 26 | -------------------------------------------------------------------------------- /section5/video_code/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | die-on-term = true 5 | module = run:app 6 | memory-report = true 7 | -------------------------------------------------------------------------------- /section6/starter_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask_restful import Api 5 | 6 | from resources.item import Item, ItemList 7 | from resources.store import Store, StoreList 8 | 9 | app = Flask(__name__) 10 | 11 | app.config['DEBUG'] = True 12 | 13 | app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') 14 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 15 | api = Api(app) 16 | 17 | 18 | api.add_resource(Store, '/store/') 19 | api.add_resource(Item, '/item/') 20 | api.add_resource(ItemList, '/items') 21 | api.add_resource(StoreList, '/stores') 22 | 23 | 24 | if __name__ == '__main__': 25 | from db import db 26 | 27 | db.init_app(app) 28 | 29 | if app.config['DEBUG']: 30 | @app.before_first_request 31 | def create_tables(): 32 | db.create_all() 33 | 34 | app.run(port=5000) 35 | -------------------------------------------------------------------------------- /section6/starter_code/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /section6/starter_code/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/models/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/models/item.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class ItemModel(db.Model): 5 | __tablename__ = 'items' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | price = db.Column(db.Float(precision=2)) 10 | 11 | store_id = db.Column(db.Integer, db.ForeignKey('stores.id')) 12 | store = db.relationship('StoreModel') 13 | 14 | def __init__(self, name, price, store_id): 15 | self.name = name 16 | self.price = price 17 | self.store_id = store_id 18 | 19 | def json(self): 20 | return {'name': self.name, 'price': self.price} 21 | 22 | @classmethod 23 | def find_by_name(cls, name): 24 | return cls.query.filter_by(name=name).first() 25 | 26 | def save_to_db(self): 27 | db.session.add(self) 28 | db.session.commit() 29 | 30 | def delete_from_db(self): 31 | db.session.delete(self) 32 | db.session.commit() 33 | -------------------------------------------------------------------------------- /section6/starter_code/models/store.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class StoreModel(db.Model): 5 | __tablename__ = 'stores' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | 10 | items = db.relationship('ItemModel', lazy='dynamic') 11 | 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | def json(self): 16 | return {'name': self.name, 'items': [item.json() for item in self.items.all()]} 17 | 18 | @classmethod 19 | def find_by_name(cls, name): 20 | return cls.query.filter_by(name=name).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def delete_from_db(self): 27 | db.session.delete(self) 28 | db.session.commit() 29 | -------------------------------------------------------------------------------- /section6/starter_code/readme.md: -------------------------------------------------------------------------------- 1 | # Stores REST Api 2 | 3 | This is built with Flask, Flask-RESTful, Flask-JWT, and Flask-SQLAlchemy. 4 | 5 | Deployed on Heroku. 6 | -------------------------------------------------------------------------------- /section6/starter_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-JWT 4 | Flask-SQLAlchemy 5 | uwsgi 6 | psycopg2 7 | -------------------------------------------------------------------------------- /section6/starter_code/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/resources/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/resources/item.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from flask_jwt import jwt_required 3 | from models.item import ItemModel 4 | 5 | 6 | class Item(Resource): 7 | parser = reqparse.RequestParser() 8 | parser.add_argument('price', 9 | type=float, 10 | required=True, 11 | help="This field cannot be left blank!") 12 | parser.add_argument('store_id', 13 | type=int, 14 | required=True, 15 | help="Every item needs a store id.") 16 | 17 | @jwt_required() 18 | def get(self, name): 19 | item = ItemModel.find_by_name(name) 20 | if item: 21 | return item.json() 22 | return {'message': 'Item not found'}, 404 23 | 24 | def post(self, name): 25 | if ItemModel.find_by_name(name): 26 | return {'message': "An item with name '{}' already exists.".format(name)}, 400 27 | 28 | data = Item.parser.parse_args() 29 | 30 | item = ItemModel(name, **data) 31 | 32 | try: 33 | item.save_to_db() 34 | except: 35 | return {"message": "An error occurred inserting the item."}, 500 36 | 37 | return item.json(), 201 38 | 39 | def delete(self, name): 40 | item = ItemModel.find_by_name(name) 41 | if item: 42 | item.delete_from_db() 43 | 44 | return {'message': 'Item deleted'} 45 | 46 | def put(self, name): 47 | data = Item.parser.parse_args() 48 | 49 | item = ItemModel.find_by_name(name) 50 | 51 | if item is None: 52 | item = ItemModel(name, **data) 53 | else: 54 | item.price = data['price'] 55 | 56 | item.save_to_db() 57 | 58 | return item.json() 59 | 60 | 61 | class ItemList(Resource): 62 | def get(self): 63 | return {'items': [x.json() for x in ItemModel.query.all()]} 64 | -------------------------------------------------------------------------------- /section6/starter_code/resources/store.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from models.store import StoreModel 3 | 4 | 5 | class Store(Resource): 6 | def get(self, name): 7 | store = StoreModel.find_by_name(name) 8 | if store: 9 | return store.json() 10 | return {'message': 'Store not found'}, 404 11 | 12 | def post(self, name): 13 | if StoreModel.find_by_name(name): 14 | return {'message': "A store with name '{}' already exists.".format(name)}, 400 15 | 16 | store = StoreModel(name) 17 | try: 18 | store.save_to_db() 19 | except: 20 | return {"message": "An error occurred creating the store."}, 500 21 | 22 | return store.json(), 201 23 | 24 | def delete(self, name): 25 | store = StoreModel.find_by_name(name) 26 | if store: 27 | store.delete_from_db() 28 | 29 | return {'message': 'Store deleted'} 30 | 31 | 32 | class StoreList(Resource): 33 | def get(self): 34 | return {'stores': [store.json() for store in StoreModel.query.all()]} 35 | -------------------------------------------------------------------------------- /section6/starter_code/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | 4 | db.init_app(app) 5 | 6 | 7 | @app.before_first_request 8 | def create_tables(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /section6/starter_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/tests/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/tests/base_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseTest 3 | 4 | This class should be the parent class to each non-unit test. 5 | It allows for instantiation of the database dynamically 6 | and makes sure that it is a new, blank database each time. 7 | """ 8 | 9 | from unittest import TestCase 10 | from app import app 11 | from db import db 12 | 13 | 14 | class BaseTest(TestCase): 15 | def setUp(self): 16 | # Make sure database exists 17 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' 18 | with app.app_context(): 19 | db.init_app(app) 20 | db.create_all() 21 | # Get a test client 22 | self.app = app.test_client() 23 | self.app_context = app.app_context 24 | 25 | def tearDown(self): 26 | # Database is blank 27 | with app.app_context(): 28 | db.session.remove() 29 | db.drop_all() 30 | -------------------------------------------------------------------------------- /section6/starter_code/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/tests/integration/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/tests/integration/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/tests/integration/models/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/tests/integration/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class ItemTest(BaseTest): 6 | def test_crud(self): 7 | with self.app_context(): 8 | item = ItemModel('test', 19.99) 9 | 10 | self.assertIsNone(ItemModel.find_by_name('test'), 11 | "Found an item with name {}, but expected not to.".format(item.name)) 12 | 13 | item.save_to_db() 14 | 15 | self.assertIsNotNone(ItemModel.find_by_name('test')) 16 | 17 | item.delete_from_db() 18 | 19 | self.assertIsNone(ItemModel.find_by_name('test')) 20 | -------------------------------------------------------------------------------- /section6/starter_code/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/tests/unit/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/tests/unit/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/starter_code/tests/unit/models/__init__.py -------------------------------------------------------------------------------- /section6/starter_code/tests/unit/models/item_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from models.item import ItemModel 4 | 5 | 6 | class ItemTest(TestCase): 7 | def test_create_item(self): 8 | item = ItemModel('test', 19.99) 9 | 10 | self.assertEqual(item.name, 'test', 11 | "The name of the item after creation does not equal the constructor argument.") 12 | self.assertEqual(item.price, 19.99, 13 | "The price of the item after creation does not equal the constructor argument.") 14 | 15 | def test_item_json(self): 16 | item = ItemModel('test', 19.99) 17 | expected = { 18 | 'name': 'test', 19 | 'price': 19.99 20 | } 21 | 22 | self.assertEqual( 23 | item.json(), 24 | expected, 25 | "The JSON export of the item is incorrect. Received {}, expected {}.".format(item.json(), expected)) 26 | -------------------------------------------------------------------------------- /section6/video_code/Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi.ini 2 | -------------------------------------------------------------------------------- /section6/video_code/README.md: -------------------------------------------------------------------------------- 1 | # Stores REST Api 2 | 3 | This is built with Flask, Flask-RESTful, Flask-JWT, and Flask-SQLAlchemy. 4 | 5 | Deployed on Heroku. 6 | -------------------------------------------------------------------------------- /section6/video_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/__init__.py -------------------------------------------------------------------------------- /section6/video_code/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask_restful import Api 5 | 6 | from resources.item import Item, ItemList 7 | from resources.store import Store, StoreList 8 | 9 | app = Flask(__name__) 10 | 11 | app.config['DEBUG'] = True 12 | 13 | app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') 14 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 15 | api = Api(app) 16 | 17 | 18 | api.add_resource(Store, '/store/') 19 | api.add_resource(Item, '/item/') 20 | api.add_resource(ItemList, '/items') 21 | api.add_resource(StoreList, '/stores') 22 | 23 | 24 | if __name__ == '__main__': 25 | from db import db 26 | 27 | db.init_app(app) 28 | 29 | if app.config['DEBUG']: 30 | @app.before_first_request 31 | def create_tables(): 32 | db.create_all() 33 | 34 | app.run(port=5000) 35 | -------------------------------------------------------------------------------- /section6/video_code/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /section6/video_code/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/models/__init__.py -------------------------------------------------------------------------------- /section6/video_code/models/item.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class ItemModel(db.Model): 5 | __tablename__ = 'items' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | price = db.Column(db.Float(precision=2)) 10 | 11 | store_id = db.Column(db.Integer, db.ForeignKey('stores.id')) 12 | store = db.relationship('StoreModel') 13 | 14 | def __init__(self, name, price, store_id): 15 | self.name = name 16 | self.price = price 17 | self.store_id = store_id 18 | 19 | def json(self): 20 | return {'name': self.name, 'price': self.price} 21 | 22 | @classmethod 23 | def find_by_name(cls, name): 24 | return cls.query.filter_by(name=name).first() 25 | 26 | def save_to_db(self): 27 | db.session.add(self) 28 | db.session.commit() 29 | 30 | def delete_from_db(self): 31 | db.session.delete(self) 32 | db.session.commit() 33 | -------------------------------------------------------------------------------- /section6/video_code/models/store.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class StoreModel(db.Model): 5 | __tablename__ = 'stores' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | 10 | items = db.relationship('ItemModel', lazy='dynamic') 11 | 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | def json(self): 16 | return {'name': self.name, 'items': [item.json() for item in self.items.all()]} 17 | 18 | @classmethod 19 | def find_by_name(cls, name): 20 | return cls.query.filter_by(name=name).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def delete_from_db(self): 27 | db.session.delete(self) 28 | db.session.commit() 29 | -------------------------------------------------------------------------------- /section6/video_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-JWT 4 | Flask-SQLAlchemy 5 | uwsgi 6 | psycopg2 7 | -------------------------------------------------------------------------------- /section6/video_code/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/resources/__init__.py -------------------------------------------------------------------------------- /section6/video_code/resources/item.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from flask_jwt import jwt_required 3 | from models.item import ItemModel 4 | 5 | 6 | class Item(Resource): 7 | parser = reqparse.RequestParser() 8 | parser.add_argument('price', 9 | type=float, 10 | required=True, 11 | help="This field cannot be left blank!") 12 | parser.add_argument('store_id', 13 | type=int, 14 | required=True, 15 | help="Every item needs a store id.") 16 | 17 | @jwt_required() 18 | def get(self, name): 19 | item = ItemModel.find_by_name(name) 20 | if item: 21 | return item.json() 22 | return {'message': 'Item not found'}, 404 23 | 24 | def post(self, name): 25 | if ItemModel.find_by_name(name): 26 | return {'message': "An item with name '{}' already exists.".format(name)}, 400 27 | 28 | data = Item.parser.parse_args() 29 | 30 | item = ItemModel(name, **data) 31 | 32 | try: 33 | item.save_to_db() 34 | except: 35 | return {"message": "An error occurred inserting the item."}, 500 36 | 37 | return item.json(), 201 38 | 39 | def delete(self, name): 40 | item = ItemModel.find_by_name(name) 41 | if item: 42 | item.delete_from_db() 43 | 44 | return {'message': 'Item deleted'} 45 | 46 | def put(self, name): 47 | data = Item.parser.parse_args() 48 | 49 | item = ItemModel.find_by_name(name) 50 | 51 | if item is None: 52 | item = ItemModel(name, **data) 53 | else: 54 | item.price = data['price'] 55 | 56 | item.save_to_db() 57 | 58 | return item.json() 59 | 60 | 61 | class ItemList(Resource): 62 | def get(self): 63 | return {'items': [x.json() for x in ItemModel.query.all()]} 64 | -------------------------------------------------------------------------------- /section6/video_code/resources/store.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from models.store import StoreModel 3 | 4 | 5 | class Store(Resource): 6 | def get(self, name): 7 | store = StoreModel.find_by_name(name) 8 | if store: 9 | return store.json() 10 | return {'message': 'Store not found'}, 404 11 | 12 | def post(self, name): 13 | if StoreModel.find_by_name(name): 14 | return {'message': "A store with name '{}' already exists.".format(name)}, 400 15 | 16 | store = StoreModel(name) 17 | try: 18 | store.save_to_db() 19 | except: 20 | return {"message": "An error occurred creating the store."}, 500 21 | 22 | return store.json(), 201 23 | 24 | def delete(self, name): 25 | store = StoreModel.find_by_name(name) 26 | if store: 27 | store.delete_from_db() 28 | 29 | return {'message': 'Store deleted'} 30 | 31 | 32 | class StoreList(Resource): 33 | def get(self): 34 | return {'stores': [store.json() for store in StoreModel.query.all()]} 35 | -------------------------------------------------------------------------------- /section6/video_code/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | 4 | db.init_app(app) 5 | 6 | 7 | @app.before_first_request 8 | def create_tables(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /section6/video_code/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.2 2 | -------------------------------------------------------------------------------- /section6/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section6/video_code/tests/base_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseTest 3 | 4 | This class should be the parent class to each unit test. 5 | It allows for instantiation of the database dynamically, 6 | and makes sure that it is a new, blank database each time. 7 | """ 8 | 9 | from unittest import TestCase 10 | from app import app 11 | from db import db 12 | 13 | 14 | class BaseTest(TestCase): 15 | SQLALCHEMY_DATABASE_URI = "sqlite://" 16 | 17 | def setUp(self): 18 | app.config['SQLALCHEMY_DATABASE_URI'] = BaseTest.SQLALCHEMY_DATABASE_URI 19 | with app.app_context(): 20 | db.init_app(app) 21 | db.create_all() 22 | self.app = app.test_client() 23 | self.app_context = app.app_context 24 | 25 | def tearDown(self): 26 | with app.app_context(): 27 | db.session.remove() 28 | db.drop_all() 29 | -------------------------------------------------------------------------------- /section6/video_code/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/tests/integration/__init__.py -------------------------------------------------------------------------------- /section6/video_code/tests/integration/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/tests/integration/models/__init__.py -------------------------------------------------------------------------------- /section6/video_code/tests/integration/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from models.store import StoreModel 3 | from tests.base_test import BaseTest 4 | 5 | 6 | class ItemTest(BaseTest): 7 | def test_crud(self): 8 | with self.app_context(): 9 | store = StoreModel('test') 10 | store.save_to_db() 11 | item = ItemModel('test', 19.99, 1) 12 | 13 | self.assertIsNone(ItemModel.find_by_name('test'), "Found an item with name 'test' before save_to_db") 14 | 15 | item.save_to_db() 16 | 17 | self.assertIsNotNone(ItemModel.find_by_name('test'), 18 | "Did not find an item with name 'test' after save_to_db") 19 | 20 | item.delete_from_db() 21 | 22 | self.assertIsNone(ItemModel.find_by_name('test'), "Found an item with name 'test' after delete_from_db") 23 | 24 | def test_store_relationship(self): 25 | with self.app_context(): 26 | store = StoreModel('test_store') 27 | item = ItemModel('test', 19.99, 1) 28 | 29 | store.save_to_db() 30 | item.save_to_db() 31 | 32 | self.assertEqual(item.store.name, 'test_store') 33 | -------------------------------------------------------------------------------- /section6/video_code/tests/integration/models/store_test.py: -------------------------------------------------------------------------------- 1 | from models.store import StoreModel 2 | from models.item import ItemModel 3 | from tests.base_test import BaseTest 4 | 5 | 6 | class StoreTest(BaseTest): 7 | def test_create_store(self): 8 | store = StoreModel('test') 9 | self.assertListEqual(store.items.all(), [], 10 | "The store's items length was not 0 even though no items were added.") 11 | 12 | def test_crud(self): 13 | with self.app_context(): 14 | store = StoreModel('test') 15 | 16 | self.assertIsNone(StoreModel.find_by_name('test'), "Found an store with name 'test' before save_to_db") 17 | 18 | store.save_to_db() 19 | 20 | self.assertIsNotNone(StoreModel.find_by_name('test'), 21 | "Did not find an store with name 'test' after save_to_db") 22 | 23 | store.delete_from_db() 24 | 25 | self.assertIsNone(StoreModel.find_by_name('test'), "Found an store with name 'test' after delete_from_db") 26 | 27 | def test_store_relationship(self): 28 | with self.app_context(): 29 | store = StoreModel('test') 30 | item = ItemModel('test_item', 19.99, 1) 31 | 32 | store.save_to_db() 33 | item.save_to_db() 34 | 35 | self.assertEqual(store.items.count(), 1) 36 | self.assertEqual(store.items.first().name, 'test_item') 37 | 38 | def test_store_json(self): 39 | store = StoreModel('test') 40 | expected = { 41 | 'name': 'test', 42 | 'items': [] 43 | } 44 | 45 | self.assertEqual( 46 | store.json(), 47 | expected, 48 | "The JSON export of the store is incorrect. Received {}, expected {}.".format(store.json(), expected)) 49 | -------------------------------------------------------------------------------- /section6/video_code/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/tests/unit/__init__.py -------------------------------------------------------------------------------- /section6/video_code/tests/unit/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section6/video_code/tests/unit/models/__init__.py -------------------------------------------------------------------------------- /section6/video_code/tests/unit/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class ItemTest(BaseTest): 6 | def test_create_item(self): 7 | item = ItemModel('test', 19.99, 1) 8 | 9 | self.assertEqual(item.name, 'test', 10 | "The name of the item after creation does not equal the constructor argument.") 11 | self.assertEqual(item.price, 19.99, 12 | "The price of the item after creation does not equal the constructor argument.") 13 | self.assertEqual(item.store_id, 1, 14 | "The store_id of the item after creation does not equal the constructor argument.") 15 | self.assertIsNone(item.store, "The item's store was not None even though the store was not created.") 16 | 17 | def test_item_json(self): 18 | item = ItemModel('test', 19.99, 1) 19 | expected = { 20 | 'name': 'test', 21 | 'price': 19.99 22 | } 23 | 24 | self.assertEqual( 25 | item.json(), 26 | expected, 27 | "The JSON export of the item is incorrect. Received {}, expected {}.".format(item.json(), expected)) 28 | -------------------------------------------------------------------------------- /section6/video_code/tests/unit/models/store_test.py: -------------------------------------------------------------------------------- 1 | from models.store import StoreModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class StoreTest(BaseTest): 6 | def test_create_store(self): 7 | store = StoreModel('test') 8 | 9 | self.assertEqual(store.name, 'test', 10 | "The name of the store after creation does not equal the constructor argument.") 11 | -------------------------------------------------------------------------------- /section6/video_code/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | die-on-term = true 5 | module = run:app 6 | memory-report = true 7 | -------------------------------------------------------------------------------- /section7/video_code/Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi.ini 2 | -------------------------------------------------------------------------------- /section7/video_code/README.md: -------------------------------------------------------------------------------- 1 | # Stores REST Api 2 | 3 | This is built with Flask, Flask-RESTful, Flask-JWT, and Flask-SQLAlchemy. 4 | 5 | Deployed on Heroku. 6 | -------------------------------------------------------------------------------- /section7/video_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/__init__.py -------------------------------------------------------------------------------- /section7/video_code/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, jsonify 4 | from flask_restful import Api 5 | from flask_jwt import JWT, JWTError 6 | 7 | from security import authenticate, identity 8 | from resources.item import Item, ItemList 9 | from resources.store import Store, StoreList 10 | from resources.user import UserRegister 11 | 12 | app = Flask(__name__) 13 | 14 | app.config['DEBUG'] = True 15 | 16 | app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') 17 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 18 | app.config['PROPAGATE_EXCEPTIONS'] = True 19 | app.secret_key = 'jose' 20 | api = Api(app) 21 | 22 | jwt = JWT(app, authenticate, identity) # /auth 23 | 24 | api.add_resource(Store, '/store/') 25 | api.add_resource(Item, '/item/') 26 | api.add_resource(ItemList, '/items') 27 | api.add_resource(StoreList, '/stores') 28 | 29 | api.add_resource(UserRegister, '/register') 30 | 31 | 32 | @app.errorhandler(JWTError) 33 | def auth_error(err): 34 | return jsonify({'message': 'Could not authorize. Did you include a valid Authorization header?'}), 401 35 | 36 | if __name__ == '__main__': 37 | from db import db 38 | 39 | db.init_app(app) 40 | 41 | if app.config['DEBUG']: 42 | @app.before_first_request 43 | def create_tables(): 44 | db.create_all() 45 | 46 | app.run(port=5000) 47 | -------------------------------------------------------------------------------- /section7/video_code/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /section7/video_code/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/models/__init__.py -------------------------------------------------------------------------------- /section7/video_code/models/item.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class ItemModel(db.Model): 5 | __tablename__ = 'items' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | price = db.Column(db.Float(precision=2)) 10 | 11 | store_id = db.Column(db.Integer, db.ForeignKey('stores.id')) 12 | store = db.relationship('StoreModel') 13 | 14 | def __init__(self, name, price, store_id): 15 | self.name = name 16 | self.price = price 17 | self.store_id = store_id 18 | 19 | def json(self): 20 | return {'name': self.name, 'price': self.price} 21 | 22 | @classmethod 23 | def find_by_name(cls, name): 24 | return cls.query.filter_by(name=name).first() 25 | 26 | def save_to_db(self): 27 | db.session.add(self) 28 | db.session.commit() 29 | 30 | def delete_from_db(self): 31 | db.session.delete(self) 32 | db.session.commit() 33 | -------------------------------------------------------------------------------- /section7/video_code/models/store.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class StoreModel(db.Model): 5 | __tablename__ = 'stores' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | 10 | items = db.relationship('ItemModel', lazy='dynamic') 11 | 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | def json(self): 16 | return {'name': self.name, 'items': [item.json() for item in self.items.all()]} 17 | 18 | @classmethod 19 | def find_by_name(cls, name): 20 | return cls.query.filter_by(name=name).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def delete_from_db(self): 27 | db.session.delete(self) 28 | db.session.commit() 29 | -------------------------------------------------------------------------------- /section7/video_code/models/user.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class UserModel(db.Model): 5 | __tablename__ = 'users' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | username = db.Column(db.String(80)) 9 | password = db.Column(db.String(80)) 10 | 11 | def __init__(self, username, password): 12 | self.username = username 13 | self.password = password 14 | 15 | def save_to_db(self): 16 | db.session.add(self) 17 | db.session.commit() 18 | 19 | @classmethod 20 | def find_by_username(cls, username): 21 | return cls.query.filter_by(username=username).first() 22 | 23 | @classmethod 24 | def find_by_id(cls, _id): 25 | return cls.query.filter_by(id=_id).first() 26 | -------------------------------------------------------------------------------- /section7/video_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-JWT 4 | Flask-SQLAlchemy 5 | uwsgi 6 | psycopg2 7 | -------------------------------------------------------------------------------- /section7/video_code/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/resources/__init__.py -------------------------------------------------------------------------------- /section7/video_code/resources/item.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from flask_jwt import jwt_required 3 | from models.item import ItemModel 4 | 5 | 6 | class Item(Resource): 7 | parser = reqparse.RequestParser() 8 | parser.add_argument('price', 9 | type=float, 10 | required=True, 11 | help="This field cannot be left blank!") 12 | parser.add_argument('store_id', 13 | type=int, 14 | required=True, 15 | help="Every item needs a store id.") 16 | 17 | @jwt_required() 18 | def get(self, name): 19 | item = ItemModel.find_by_name(name) 20 | if item: 21 | return item.json() 22 | return {'message': 'Item not found'}, 404 23 | 24 | def post(self, name): 25 | if ItemModel.find_by_name(name): 26 | return {'message': "An item with name '{}' already exists.".format(name)}, 400 27 | 28 | data = Item.parser.parse_args() 29 | 30 | item = ItemModel(name, **data) 31 | 32 | try: 33 | item.save_to_db() 34 | except: 35 | return {"message": "An error occurred inserting the item."}, 500 36 | 37 | return item.json(), 201 38 | 39 | def delete(self, name): 40 | item = ItemModel.find_by_name(name) 41 | if item: 42 | item.delete_from_db() 43 | 44 | return {'message': 'Item deleted'} 45 | 46 | def put(self, name): 47 | data = Item.parser.parse_args() 48 | 49 | item = ItemModel.find_by_name(name) 50 | 51 | if item is None: 52 | item = ItemModel(name, **data) 53 | else: 54 | item.price = data['price'] 55 | 56 | item.save_to_db() 57 | 58 | return item.json() 59 | 60 | 61 | class ItemList(Resource): 62 | def get(self): 63 | return {'items': [x.json() for x in ItemModel.query.all()]} 64 | -------------------------------------------------------------------------------- /section7/video_code/resources/store.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from models.store import StoreModel 3 | 4 | 5 | class Store(Resource): 6 | def get(self, name): 7 | store = StoreModel.find_by_name(name) 8 | if store: 9 | return store.json() 10 | return {'message': 'Store not found'}, 404 11 | 12 | def post(self, name): 13 | if StoreModel.find_by_name(name): 14 | return {'message': "A store with name '{}' already exists.".format(name)}, 400 15 | 16 | store = StoreModel(name) 17 | try: 18 | store.save_to_db() 19 | except: 20 | return {"message": "An error occurred creating the store."}, 500 21 | 22 | return store.json(), 201 23 | 24 | def delete(self, name): 25 | store = StoreModel.find_by_name(name) 26 | if store: 27 | store.delete_from_db() 28 | 29 | return {'message': 'Store deleted'} 30 | 31 | 32 | class StoreList(Resource): 33 | def get(self): 34 | return {'stores': [store.json() for store in StoreModel.query.all()]} 35 | -------------------------------------------------------------------------------- /section7/video_code/resources/user.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from models.user import UserModel 3 | 4 | 5 | class UserRegister(Resource): 6 | parser = reqparse.RequestParser() 7 | parser.add_argument('username', 8 | type=str, 9 | required=True, 10 | help="This field cannot be blank.") 11 | parser.add_argument('password', 12 | type=str, 13 | required=True, 14 | help="This field cannot be blank.") 15 | 16 | def post(self): 17 | data = UserRegister.parser.parse_args() 18 | 19 | if UserModel.find_by_username(data['username']): 20 | return {"message": "A user with that username already exists"}, 400 21 | 22 | user = UserModel(**data) 23 | user.save_to_db() 24 | 25 | return {"message": "User created successfully."}, 201 26 | -------------------------------------------------------------------------------- /section7/video_code/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | 4 | db.init_app(app) 5 | 6 | 7 | @app.before_first_request 8 | def create_tables(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /section7/video_code/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.2 2 | -------------------------------------------------------------------------------- /section7/video_code/security.py: -------------------------------------------------------------------------------- 1 | from hmac import compare_digest 2 | from models.user import UserModel 3 | 4 | 5 | def authenticate(username, password): 6 | user = UserModel.find_by_username(username) 7 | if user and compare_digest(user.password, password): 8 | return user 9 | 10 | 11 | def identity(payload): 12 | user_id = payload['identity'] 13 | return UserModel.find_by_id(user_id) 14 | -------------------------------------------------------------------------------- /section7/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section7/video_code/tests/base_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseTest 3 | 4 | This class should be the parent class to each unit test. 5 | It allows for instantiation of the database dynamically, 6 | and makes sure that it is a new, blank database each time. 7 | """ 8 | 9 | from unittest import TestCase 10 | from app import app 11 | from db import db 12 | 13 | 14 | class BaseTest(TestCase): 15 | SQLALCHEMY_DATABASE_URI = "sqlite://" 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | app.config['SQLALCHEMY_DATABASE_URI'] = BaseTest.SQLALCHEMY_DATABASE_URI 20 | app.config['DEBUG'] = False 21 | with app.app_context(): 22 | db.init_app(app) 23 | 24 | def setUp(self): 25 | with app.app_context(): 26 | db.create_all() 27 | self.app = app.test_client 28 | self.app_context = app.app_context 29 | 30 | def tearDown(self): 31 | with app.app_context(): 32 | db.session.remove() 33 | db.drop_all() 34 | -------------------------------------------------------------------------------- /section7/video_code/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/tests/integration/__init__.py -------------------------------------------------------------------------------- /section7/video_code/tests/integration/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/tests/integration/models/__init__.py -------------------------------------------------------------------------------- /section7/video_code/tests/integration/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from models.store import StoreModel 3 | from tests.base_test import BaseTest 4 | 5 | 6 | class ItemTest(BaseTest): 7 | def test_crud(self): 8 | with self.app_context(): 9 | store = StoreModel('test') 10 | store.save_to_db() 11 | item = ItemModel('test', 19.99, 1) 12 | 13 | self.assertIsNone(ItemModel.find_by_name('test'), "Found an item with name 'test' before save_to_db") 14 | 15 | item.save_to_db() 16 | 17 | self.assertIsNotNone(ItemModel.find_by_name('test'), 18 | "Did not find an item with name 'test' after save_to_db") 19 | 20 | item.delete_from_db() 21 | 22 | self.assertIsNone(ItemModel.find_by_name('test'), "Found an item with name 'test' after delete_from_db") 23 | 24 | def test_store_relationship(self): 25 | with self.app_context(): 26 | store = StoreModel('test_store') 27 | item = ItemModel('test', 19.99, 1) 28 | 29 | store.save_to_db() 30 | item.save_to_db() 31 | 32 | self.assertEqual(item.store.name, 'test_store') 33 | -------------------------------------------------------------------------------- /section7/video_code/tests/integration/models/store_test.py: -------------------------------------------------------------------------------- 1 | from models.store import StoreModel 2 | from models.item import ItemModel 3 | from tests.base_test import BaseTest 4 | 5 | 6 | class StoreTest(BaseTest): 7 | def test_crud(self): 8 | with self.app_context(): 9 | store = StoreModel('test') 10 | 11 | self.assertIsNone(StoreModel.find_by_name('test'), "Found an store with name 'test' before save_to_db") 12 | 13 | store.save_to_db() 14 | 15 | self.assertIsNotNone(StoreModel.find_by_name('test'), 16 | "Did not find an store with name 'test' after save_to_db") 17 | 18 | store.delete_from_db() 19 | 20 | self.assertIsNone(StoreModel.find_by_name('test'), "Found an store with name 'test' after delete_from_db") 21 | 22 | def test_store_relationship(self): 23 | with self.app_context(): 24 | store = StoreModel('test') 25 | item = ItemModel('test_item', 19.99, 1) 26 | 27 | store.save_to_db() 28 | item.save_to_db() 29 | 30 | self.assertEqual(store.items.count(), 1) 31 | self.assertEqual(store.items.first().name, 'test_item') 32 | -------------------------------------------------------------------------------- /section7/video_code/tests/integration/models/user_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class UserTest(BaseTest): 6 | def test_crud(self): 7 | with self.app_context(): 8 | user = UserModel('test', 'abcd') 9 | 10 | self.assertIsNone(UserModel.find_by_username('test'), "Found an user with name 'test' before save_to_db") 11 | self.assertIsNone(UserModel.find_by_id(1), "Found an user with id '1' before save_to_db") 12 | 13 | user.save_to_db() 14 | 15 | self.assertIsNotNone(UserModel.find_by_username('test'), 16 | "Did not find an user with name 'test' after save_to_db") 17 | self.assertIsNotNone(UserModel.find_by_id(1), "Did not find an user with id '1' after save_to_db") 18 | -------------------------------------------------------------------------------- /section7/video_code/tests/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/tests/system/__init__.py -------------------------------------------------------------------------------- /section7/video_code/tests/system/item_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from models.item import ItemModel 3 | from models.store import StoreModel 4 | from tests.base_test import BaseTest 5 | import json 6 | 7 | 8 | class ItemTest(BaseTest): 9 | def setUp(self): 10 | super(ItemTest, self).setUp() 11 | with self.app() as c: 12 | with self.app_context(): 13 | UserModel('test', '1234').save_to_db() 14 | auth_request = c.post('/auth', data=json.dumps({ 15 | 'username': 'test', 16 | 'password': '1234' 17 | }), headers={'Content-Type': 'application/json'}) 18 | self.auth_header = "JWT {}".format(json.loads(auth_request.data)['access_token']) 19 | 20 | def test_item_no_auth(self): 21 | with self.app() as c: 22 | r = c.get('/item/test') 23 | self.assertEqual(r.status_code, 401) 24 | 25 | def test_item_not_found(self): 26 | with self.app() as c: 27 | r = c.get('/item/test', headers={'Authorization': self.auth_header}) 28 | self.assertEqual(r.status_code, 404) 29 | 30 | def test_item_found(self): 31 | with self.app() as c: 32 | with self.app_context(): 33 | StoreModel('test').save_to_db() 34 | ItemModel('test', 17.99, 1).save_to_db() 35 | r = c.get('/item/test', headers={'Authorization': self.auth_header}) 36 | 37 | self.assertEqual(r.status_code, 200) 38 | self.assertDictEqual(d1={'name': 'test', 'price': 17.99}, 39 | d2=json.loads(r.data)) 40 | 41 | def test_delete_item(self): 42 | with self.app() as c: 43 | with self.app_context(): 44 | StoreModel('test').save_to_db() 45 | ItemModel('test', 17.99, 1).save_to_db() 46 | r = c.delete('/item/test') 47 | 48 | self.assertEqual(r.status_code, 200) 49 | self.assertDictEqual(d1={'message': 'Item deleted'}, 50 | d2=json.loads(r.data)) 51 | 52 | def test_create_item(self): 53 | with self.app() as c: 54 | with self.app_context(): 55 | StoreModel('test').save_to_db() 56 | r = c.post('/item/test', data={'price': 17.99, 'store_id': 1}) 57 | 58 | self.assertEqual(r.status_code, 201) 59 | self.assertEqual(ItemModel.find_by_name('test').price, 17.99) 60 | self.assertDictEqual(d1={'name': 'test', 'price': 17.99}, 61 | d2=json.loads(r.data)) 62 | 63 | def test_create_duplicate_item(self): 64 | with self.app() as c: 65 | with self.app_context(): 66 | StoreModel('test').save_to_db() 67 | c.post('/item/test', data={'price': 17.99, 'store_id': 1}) 68 | r = c.post('/item/test', data={'price': 17.99, 'store_id': 1}) 69 | 70 | self.assertEqual(r.status_code, 400) 71 | 72 | def test_put_item(self): 73 | with self.app() as c: 74 | with self.app_context(): 75 | StoreModel('test').save_to_db() 76 | r = c.put('/item/test', data={'price': 17.99, 'store_id': 1}) 77 | 78 | self.assertEqual(r.status_code, 200) 79 | self.assertEqual(ItemModel.find_by_name('test').price, 17.99) 80 | self.assertDictEqual(d1={'name': 'test', 'price': 17.99}, 81 | d2=json.loads(r.data)) 82 | 83 | def test_put_update_item(self): 84 | with self.app() as c: 85 | with self.app_context(): 86 | StoreModel('test').save_to_db() 87 | c.put('/item/test', data={'price': 17.99, 'store_id': 1}) 88 | r = c.put('/item/test', data={'price': 18.99, 'store_id': 1}) 89 | 90 | self.assertEqual(r.status_code, 200) 91 | self.assertEqual(ItemModel.find_by_name('test').price, 18.99) 92 | 93 | def test_item_list(self): 94 | with self.app() as c: 95 | with self.app_context(): 96 | StoreModel('test').save_to_db() 97 | ItemModel('test', 17.99, 1).save_to_db() 98 | r = c.get('/items') 99 | 100 | self.assertDictEqual(d1={'items': [{'name': 'test', 'price': 17.99}]}, 101 | d2=json.loads(r.data)) 102 | -------------------------------------------------------------------------------- /section7/video_code/tests/system/store_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from models.store import StoreModel 3 | from tests.base_test import BaseTest 4 | import json 5 | 6 | 7 | class StoreTest(BaseTest): 8 | def test_store_not_found(self): 9 | with self.app() as c: 10 | r = c.get('/store/test') 11 | self.assertEqual(r.status_code, 404) 12 | 13 | def test_store_found(self): 14 | with self.app() as c: 15 | with self.app_context(): 16 | StoreModel('test').save_to_db() 17 | r = c.get('/store/test') 18 | 19 | self.assertEqual(r.status_code, 200) 20 | self.assertDictEqual(d1={'name': 'test', 'items': []}, 21 | d2=json.loads(r.data)) 22 | 23 | def test_store_with_items_found(self): 24 | with self.app() as c: 25 | with self.app_context(): 26 | StoreModel('test').save_to_db() 27 | ItemModel('test', 2.99, 1).save_to_db() 28 | r = c.get('/store/test') 29 | 30 | self.assertEqual(r.status_code, 200) 31 | self.assertDictEqual(d1={'name': 'test', 'items': [{'name': 'test', 'price': 2.99}]}, 32 | d2=json.loads(r.data)) 33 | 34 | def test_delete_store(self): 35 | with self.app() as c: 36 | with self.app_context(): 37 | StoreModel('test').save_to_db() 38 | r = c.delete('/store/test') 39 | 40 | self.assertEqual(r.status_code, 200) 41 | self.assertDictEqual(d1={'message': 'Store deleted'}, 42 | d2=json.loads(r.data)) 43 | 44 | def test_create_store(self): 45 | with self.app() as c: 46 | with self.app_context(): 47 | r = c.post('/store/test') 48 | 49 | self.assertEqual(r.status_code, 201) 50 | self.assertIsNotNone(StoreModel.find_by_name('test')) 51 | self.assertDictEqual(d1={'name': 'test', 'items': []}, 52 | d2=json.loads(r.data)) 53 | 54 | def test_create_duplicate_store(self): 55 | with self.app() as c: 56 | with self.app_context(): 57 | c.post('/store/test') 58 | r = c.post('/store/test') 59 | 60 | self.assertEqual(r.status_code, 400) 61 | 62 | def test_store_list(self): 63 | with self.app() as c: 64 | with self.app_context(): 65 | StoreModel('test').save_to_db() 66 | r = c.get('/stores') 67 | 68 | self.assertDictEqual(d1={'stores': [{'name': 'test', 'items': []}]}, 69 | d2=json.loads(r.data)) 70 | 71 | def test_store_with_items_list(self): 72 | with self.app() as c: 73 | with self.app_context(): 74 | StoreModel('test').save_to_db() 75 | ItemModel('test', 17.99, 1).save_to_db() 76 | r = c.get('/stores') 77 | 78 | self.assertDictEqual(d1={'stores': [{'name': 'test', 'items': [{'name': 'test', 'price': 17.99}]}]}, 79 | d2=json.loads(r.data)) 80 | -------------------------------------------------------------------------------- /section7/video_code/tests/system/user_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from tests.base_test import BaseTest 3 | import json 4 | 5 | 6 | class UserTest(BaseTest): 7 | def test_register_user(self): 8 | with self.app() as c: 9 | with self.app_context(): 10 | r = c.post('/register', data={'username': 'test', 'password': '1234'}) 11 | 12 | self.assertEqual(r.status_code, 201) 13 | self.assertIsNotNone(UserModel.find_by_username('test')) 14 | self.assertDictEqual(d1={'message': 'User created successfully.'}, 15 | d2=json.loads(r.data)) 16 | 17 | def test_register_and_login(self): 18 | with self.app() as c: 19 | with self.app_context(): 20 | c.post('/register', data={'username': 'test', 'password': '1234'}) 21 | auth_request = c.post('/auth', data=json.dumps({ 22 | 'username': 'test', 23 | 'password': '1234' 24 | }), headers={'Content-Type': 'application/json'}) 25 | 26 | self.assertIn('access_token', json.loads(auth_request.data).keys()) 27 | 28 | def test_register_duplicate_user(self): 29 | with self.app() as c: 30 | with self.app_context(): 31 | c.post('/register', data={'username': 'test', 'password': '1234'}) 32 | r = c.post('/register', data={'username': 'test', 'password': '1234'}) 33 | 34 | self.assertEqual(r.status_code, 400) 35 | self.assertDictEqual(d1={'message': 'A user with that username already exists'}, 36 | d2=json.loads(r.data)) 37 | -------------------------------------------------------------------------------- /section7/video_code/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/tests/unit/__init__.py -------------------------------------------------------------------------------- /section7/video_code/tests/unit/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section7/video_code/tests/unit/models/__init__.py -------------------------------------------------------------------------------- /section7/video_code/tests/unit/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class ItemTest(BaseTest): 6 | def test_create_item(self): 7 | item = ItemModel('test', 19.99, 1) 8 | 9 | self.assertEqual(item.name, 'test', 10 | "The name of the item after creation does not equal the constructor argument.") 11 | self.assertEqual(item.price, 19.99, 12 | "The price of the item after creation does not equal the constructor argument.") 13 | self.assertEqual(item.store_id, 1, 14 | "The store_id of the item after creation does not equal the constructor argument.") 15 | self.assertIsNone(item.store, "The item's store was not None even though the store was not created.") 16 | 17 | def test_item_json(self): 18 | item = ItemModel('test', 19.99, 1) 19 | expected = { 20 | 'name': 'test', 21 | 'price': 19.99 22 | } 23 | 24 | self.assertEqual( 25 | item.json(), 26 | expected, 27 | "The JSON export of the item is incorrect. Received {}, expected {}.".format(item.json(), expected)) 28 | -------------------------------------------------------------------------------- /section7/video_code/tests/unit/models/store_test.py: -------------------------------------------------------------------------------- 1 | from models.store import StoreModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class StoreTest(BaseTest): 6 | def test_create_store(self): 7 | store = StoreModel('test') 8 | 9 | self.assertEqual(store.name, 'test', 10 | "The name of the store after creation does not equal the constructor argument.") 11 | self.assertListEqual(store.items.all(), [], 12 | "The store's items length was not 0 even though no items were added.") 13 | 14 | def test_store_json(self): 15 | store = StoreModel('test') 16 | expected = { 17 | 'name': 'test', 18 | 'items': [] 19 | } 20 | 21 | self.assertEqual( 22 | store.json(), 23 | expected, 24 | "The JSON export of the store is incorrect. Received {}, expected {}.".format(store.json(), expected)) 25 | -------------------------------------------------------------------------------- /section7/video_code/tests/unit/models/user_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class UserTest(BaseTest): 6 | def test_create_user(self): 7 | user = UserModel('test', 'abcd') 8 | 9 | self.assertEqual(user.username, 'test', 10 | "The name of the user after creation does not equal the constructor argument.") 11 | self.assertEqual(user.password, 'abcd', 12 | "The password of the user after creation does not equal the constructor argument.") 13 | -------------------------------------------------------------------------------- /section7/video_code/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | die-on-term = true 5 | module = run:app 6 | memory-report = true 7 | -------------------------------------------------------------------------------- /section8/README.md: -------------------------------------------------------------------------------- 1 | # System Testing with Postman and Newman 2 | 3 | In this section, we learn about how to use Postman to write system tests, and how to run them in the command line with Newman. 4 | 5 | Here's an overview of how the PyCharm Run Configuration flow goes: 6 | 7 | ![PyCharm Run Configuration Flow](./assets/pycharm-run-configuration-flow-export-22-03-2024-11_38_55.png) 8 | 9 | ## Windows-specific information 10 | 11 | ### Bash Support no longer needed 12 | 13 | In the videos we use the **Bash Support** extension of PyCharm to interact with the command line. Specifically, we use it to create **Run Configurations** so that we can hit the "Play" button and that runs a command-line script. 14 | 15 | This is no longer necessary, as PyCharm now ships with **Shell Script**. You can skip installing **Bash Support**, and where we use **Bash Support** in the videos, you can use **Shell Script**. 16 | 17 | > **Key Takeaway**: don't use **Bash Support**, instead use the built-in **Shell Script**. 18 | 19 | ### Installing nvm (Node Version Manager) 20 | 21 | ![Overview of running tests with Newman](./assets/overview-of-running-tests-with-newman-export-22-03-2024-11_38_55.png) 22 | 23 | In the videos we install Node Version Manager (nvm) to install a specific version of NodeJS. We use that to install Newman, which we can use to run Postman tests in the command line. 24 | 25 | If you are using Windows, you'll want to install [nvm-windows](https://github.com/coreybutler/nvm-windows) instead. There's a Windows installer you can use: [github.com/coreybutler/nvm-windows/releases](https://github.com/coreybutler/nvm-windows/releases) . Download the `nvm-setup.exe` file and execute it. Once it has finished installing, you'll be able to run `nvm` in your `cmd.exe` application. 26 | 27 | Other alternatives include [nodeenv](https://github.com/ekalinin/nodeenv), [nodist](https://github.com/marcelklehr/nodist), or [nvs](https://github.com/jasongin/nvs). 28 | 29 | There, you can see the available NodeJS versions by running: 30 | 31 | ```bash 32 | nvm list available 33 | ``` 34 | 35 | Then you can install the latest LTS (Long-Term Support) version: 36 | 37 | ```bash 38 | nvm install lts 39 | ``` 40 | 41 | Once installed, you can use: 42 | 43 | ```bash 44 | nvm use lts 45 | ``` 46 | 47 | Then, install newman: 48 | 49 | ```bash 50 | npm install newman 51 | ``` 52 | 53 | Finally, you'll need to know where the `newman` executable has been placed. You can find out its location with this command: 54 | 55 | ```bash 56 | where newman 57 | ``` 58 | 59 | Copy the path to the `newman` executable, as you'll need it in PyCharm when you create a **Shell Script** Run Configuration. Use the path in the **Script text** field of the Run Configuration. 60 | 61 | ### Running newman from PyCharm 62 | 63 | 1. Enable the Shell Script Plugin by going into your Settings -> Plugins -> Shell Script. 64 | 2. Create a new Run Configuration by going to the top right dropdown, clicking it, and then clicking on "Edit Configurations..." 65 | 3. Create a new Run Configuration of type Shell Script and give it a name, such as "Run Newman tests" 66 | 4. Select "Script Text" 67 | 5. In the Script text field, type your path to newman followed by `run stores-rest-api.postman_collection.json` . 68 | 1. **Important** if your path to newman contains spaces, then write it in this format: `& 'path to your newman'` 69 | 2. The final value of the Script text field will be something like this: `& 'path to your newman' run stores-rest-api.postman_collection.json` . 70 | 6. Press OK 71 | 72 | You can follow the videos as normal for using the Multirun Run Configuration to start the app and run the tests. 73 | -------------------------------------------------------------------------------- /section8/assets/overview-of-running-tests-with-newman-export-22-03-2024-11_38_55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/assets/overview-of-running-tests-with-newman-export-22-03-2024-11_38_55.png -------------------------------------------------------------------------------- /section8/assets/pycharm-run-configuration-flow-export-22-03-2024-11_38_55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/assets/pycharm-run-configuration-flow-export-22-03-2024-11_38_55.png -------------------------------------------------------------------------------- /section8/stores-rest-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "Stores REST API", 5 | "_postman_id": "1dfbadf9-2c3a-832d-35fb-512202c171a8", 6 | "description": "", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "User create store and item", 12 | "description": "Check user can register.\nCheck user can create store.\nCheck user can create item in store.", 13 | "item": [ 14 | { 15 | "name": "/register", 16 | "event": [ 17 | { 18 | "listen": "test", 19 | "script": { 20 | "type": "text/javascript", 21 | "exec": [ 22 | "postman.clearEnvironmentVariable(\"access_token\");", 23 | "", 24 | "var jsonData = JSON.parse(responseBody);", 25 | "tests[\"User created successfully\"] = jsonData.message === 'User created successfully.';", 26 | "", 27 | "tests[\"Response time is less than 200ms\"] = responseTime < 200;", 28 | "", 29 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 30 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';" 31 | ] 32 | } 33 | } 34 | ], 35 | "request": { 36 | "url": "{{url}}/register", 37 | "method": "POST", 38 | "header": [ 39 | { 40 | "key": "Content-Type", 41 | "value": "application/json", 42 | "description": "" 43 | } 44 | ], 45 | "body": { 46 | "mode": "raw", 47 | "raw": "{\n\t\"username\": \"user1\",\n\t\"password\": \"abcxyz\"\n}" 48 | }, 49 | "description": "" 50 | }, 51 | "response": [] 52 | }, 53 | { 54 | "name": "/auth", 55 | "event": [ 56 | { 57 | "listen": "test", 58 | "script": { 59 | "type": "text/javascript", 60 | "exec": [ 61 | "var jsonData = JSON.parse(responseBody);", 62 | "", 63 | "postman.setEnvironmentVariable(\"access_token\", jsonData.access_token);", 64 | "tests[\"Status code is 200\"] = responseCode.code === 200;", 65 | "tests[\"Body contains access_token\"] = responseBody.has(\"access_token\");", 66 | "", 67 | "tests[\"Response time is less than 150ms\"] = responseTime < 150;", 68 | "", 69 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 70 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';" 71 | ] 72 | } 73 | } 74 | ], 75 | "request": { 76 | "url": "{{url}}/auth", 77 | "method": "POST", 78 | "header": [ 79 | { 80 | "key": "Content-Type", 81 | "value": "application/json", 82 | "description": "" 83 | } 84 | ], 85 | "body": { 86 | "mode": "raw", 87 | "raw": "{\n\t\"username\": \"user1\",\n\t\"password\": \"abcxyz\"\n}" 88 | }, 89 | "description": "" 90 | }, 91 | "response": [] 92 | }, 93 | { 94 | "name": "/store/test_store", 95 | "event": [ 96 | { 97 | "listen": "test", 98 | "script": { 99 | "type": "text/javascript", 100 | "exec": [ 101 | "var jsonData = JSON.parse(responseBody);", 102 | "tests[\"Store name is returned\"] = jsonData.name === 'test_store';", 103 | "tests[\"Store id is returned\"] = jsonData.id === 1;", 104 | "tests[\"Store items are an empty list\"] = jsonData.items.length === 0;", 105 | "", 106 | "tests[\"Successful POST request\"] = responseCode.code === 201;", 107 | "tests[\"Response time is less than 200ms\"] = responseTime < 200;", 108 | "", 109 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 110 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';", 111 | "", 112 | "postman.setEnvironmentVariable(\"store_id\", jsonData.id);" 113 | ] 114 | } 115 | } 116 | ], 117 | "request": { 118 | "url": "{{url}}/store/test_store", 119 | "method": "POST", 120 | "header": [], 121 | "body": {}, 122 | "description": "" 123 | }, 124 | "response": [] 125 | }, 126 | { 127 | "name": "/item/test_item in test_store", 128 | "event": [ 129 | { 130 | "listen": "test", 131 | "script": { 132 | "type": "text/javascript", 133 | "exec": [ 134 | "var jsonData = JSON.parse(responseBody);", 135 | "tests[\"Item name is returned\"] = jsonData.name === 'test_item';", 136 | "tests[\"Item price is returned\"] = jsonData.price === 17.99;", 137 | "tests[\"Successful POST request\"] = responseCode.code === 201;", 138 | "tests[\"Response time is less than 200ms\"] = responseTime < 200;", 139 | "", 140 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 141 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';" 142 | ] 143 | } 144 | } 145 | ], 146 | "request": { 147 | "url": "{{url}}/item/test_item", 148 | "method": "POST", 149 | "header": [ 150 | { 151 | "key": "Content-Type", 152 | "value": "application/json", 153 | "description": "" 154 | } 155 | ], 156 | "body": { 157 | "mode": "raw", 158 | "raw": "{\n\t\"price\": 17.99,\n\t\"store_id\": {{store_id}}\n}" 159 | }, 160 | "description": "" 161 | }, 162 | "response": [] 163 | }, 164 | { 165 | "name": "/stores", 166 | "event": [ 167 | { 168 | "listen": "test", 169 | "script": { 170 | "type": "text/javascript", 171 | "exec": [ 172 | "var jsonData = JSON.parse(responseBody);", 173 | "tests[\"Store 'test_store' is returned\"] = jsonData.stores[0].name === 'test_store';", 174 | "tests[\"ID of store 'test_store' is returned\"] = jsonData.stores[0].id === parseInt(environment.store_id);", 175 | "tests[\"Item 'test_item' is returned inside 'test_store'\"] = jsonData.stores[0].items[0].name === 'test_item';", 176 | "tests[\"Item 'test_item' price is returned inside 'test_store'\"] = jsonData.stores[0].items[0].price === 17.99;", 177 | "", 178 | "", 179 | "tests[\"Response time is less than 200ms\"] = responseTime < 200;", 180 | "", 181 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 182 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';" 183 | ] 184 | } 185 | } 186 | ], 187 | "request": { 188 | "url": "{{url}}/stores", 189 | "method": "GET", 190 | "header": [], 191 | "body": {}, 192 | "description": "" 193 | }, 194 | "response": [] 195 | }, 196 | { 197 | "name": "/item/my_item copy", 198 | "event": [ 199 | { 200 | "listen": "test", 201 | "script": { 202 | "type": "text/javascript", 203 | "exec": [ 204 | "var jsonData = JSON.parse(responseBody);", 205 | "tests[\"Message is returned\"] = jsonData.message === 'Item deleted';", 206 | "tests[\"Successful POST request\"] = responseCode.code === 200;", 207 | "tests[\"Response time is less than 200ms\"] = responseTime < 200;", 208 | "", 209 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 210 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';" 211 | ] 212 | } 213 | } 214 | ], 215 | "request": { 216 | "url": "{{url}}/item/test_item", 217 | "method": "DELETE", 218 | "header": [], 219 | "body": {}, 220 | "description": "" 221 | }, 222 | "response": [] 223 | }, 224 | { 225 | "name": "/store/ copy", 226 | "event": [ 227 | { 228 | "listen": "test", 229 | "script": { 230 | "type": "text/javascript", 231 | "exec": [ 232 | "var jsonData = JSON.parse(responseBody);", 233 | "tests[\"Store name is returned\"] = jsonData.message === 'Store deleted';", 234 | "tests[\"Successful POST request\"] = responseCode.code === 200;", 235 | "tests[\"Response time is less than 200ms\"] = responseTime < 200;", 236 | "", 237 | "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", 238 | "tests[\"Content-Type is 'application/json'\"] = postman.getResponseHeader(\"Content-Type\") === 'application/json';" 239 | ] 240 | } 241 | } 242 | ], 243 | "request": { 244 | "url": "{{url}}/store/test_store", 245 | "method": "DELETE", 246 | "header": [], 247 | "body": {}, 248 | "description": "" 249 | }, 250 | "response": [] 251 | } 252 | ] 253 | }, 254 | { 255 | "name": "/register", 256 | "request": { 257 | "url": "{{url}}/register", 258 | "method": "POST", 259 | "header": [ 260 | { 261 | "key": "Content-Type", 262 | "value": "application/json", 263 | "description": "" 264 | }, 265 | { 266 | "key": "Authorization", 267 | "value": "JWT", 268 | "description": "" 269 | } 270 | ], 271 | "body": { 272 | "mode": "raw", 273 | "raw": "{\n\t\"username\": \"user1\",\n\t\"password\": \"abcxyz\"\n}" 274 | }, 275 | "description": "" 276 | }, 277 | "response": [] 278 | }, 279 | { 280 | "name": "/auth", 281 | "event": [ 282 | { 283 | "listen": "test", 284 | "script": { 285 | "type": "text/javascript", 286 | "exec": [ 287 | "var jsonData = JSON.parse(responseBody);", 288 | "", 289 | "postman.setEnvironmentVariable(\"access_token\", jsonData.access_token);" 290 | ] 291 | } 292 | } 293 | ], 294 | "request": { 295 | "url": "{{url}}/auth", 296 | "method": "POST", 297 | "header": [ 298 | { 299 | "key": "Content-Type", 300 | "value": "application/json", 301 | "description": "" 302 | }, 303 | { 304 | "key": "Authorization", 305 | "value": "JWT", 306 | "description": "" 307 | } 308 | ], 309 | "body": { 310 | "mode": "raw", 311 | "raw": "{\n\t\"username\": \"user1\",\n\t\"password\": \"abcxyz\"\n}" 312 | }, 313 | "description": "" 314 | }, 315 | "response": [] 316 | }, 317 | { 318 | "name": "/stores", 319 | "request": { 320 | "url": "{{url}}/stores", 321 | "method": "GET", 322 | "header": [], 323 | "body": {}, 324 | "description": "" 325 | }, 326 | "response": [] 327 | }, 328 | { 329 | "name": "/store/", 330 | "request": { 331 | "url": "{{url}}/store/my_store", 332 | "method": "GET", 333 | "header": [], 334 | "body": {}, 335 | "description": "" 336 | }, 337 | "response": [] 338 | }, 339 | { 340 | "name": "/store/", 341 | "request": { 342 | "url": "{{url}}/store/my_store", 343 | "method": "POST", 344 | "header": [], 345 | "body": {}, 346 | "description": "" 347 | }, 348 | "response": [] 349 | }, 350 | { 351 | "name": "/store/", 352 | "request": { 353 | "url": "{{url}}/store/my_store", 354 | "method": "DELETE", 355 | "header": [], 356 | "body": {}, 357 | "description": "" 358 | }, 359 | "response": [] 360 | }, 361 | { 362 | "name": "/item/", 363 | "request": { 364 | "url": "{{url}}/item/my_item", 365 | "method": "POST", 366 | "header": [ 367 | { 368 | "key": "Content-Type", 369 | "value": "application/json", 370 | "description": "" 371 | } 372 | ], 373 | "body": { 374 | "mode": "raw", 375 | "raw": "{\n\t\"price\": 17.99,\n\t\"store_id\": 3\n}" 376 | }, 377 | "description": "" 378 | }, 379 | "response": [] 380 | }, 381 | { 382 | "name": "/item/my_item", 383 | "request": { 384 | "url": "{{url}}/item/my_item", 385 | "method": "GET", 386 | "header": [ 387 | { 388 | "key": "Authorization", 389 | "value": "JWT {{access_token}}", 390 | "description": "" 391 | } 392 | ], 393 | "body": {}, 394 | "description": "" 395 | }, 396 | "response": [] 397 | }, 398 | { 399 | "name": "/item/my_item", 400 | "request": { 401 | "url": "{{url}}/item/my_item", 402 | "method": "DELETE", 403 | "header": [], 404 | "body": {}, 405 | "description": "" 406 | }, 407 | "response": [] 408 | }, 409 | { 410 | "name": "/items", 411 | "request": { 412 | "url": "{{url}}/items", 413 | "method": "GET", 414 | "header": [], 415 | "body": {}, 416 | "description": "" 417 | }, 418 | "response": [] 419 | } 420 | ] 421 | } -------------------------------------------------------------------------------- /section8/stores-rest-api.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "b274afdb-720f-ff67-ab20-ca64592301b8", 3 | "name": "Stores REST API", 4 | "values": [ 5 | { 6 | "enabled": true, 7 | "key": "url", 8 | "value": "http://127.0.0.1:5000", 9 | "type": "text" 10 | }, 11 | { 12 | "enabled": true, 13 | "key": "access_token", 14 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTIzNDkwMjYsImlhdCI6MTQ5MjM0ODcyNiwibmJmIjoxNDkyMzQ4NzI2LCJpZGVudGl0eSI6MX0.P5Ax6M396lVRQx2O1G10eCHBcPZkCeaLJ10d1YeM2PE", 15 | "type": "text" 16 | }, 17 | { 18 | "enabled": true, 19 | "key": "store_id", 20 | "value": "1", 21 | "type": "text" 22 | } 23 | ], 24 | "timestamp": 1492348726807, 25 | "_postman_variable_scope": "environment", 26 | "_postman_exported_at": "2017-04-22T08:52:27.820Z", 27 | "_postman_exported_using": "Postman/4.10.7" 28 | } -------------------------------------------------------------------------------- /section8/video_code/Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi.ini 2 | -------------------------------------------------------------------------------- /section8/video_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/__init__.py -------------------------------------------------------------------------------- /section8/video_code/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, jsonify 4 | from flask_restful import Api 5 | from flask_jwt import JWT, JWTError 6 | 7 | from security import authenticate, identity 8 | from resources.user import UserRegister 9 | from resources.item import Item, ItemList 10 | from resources.store import Store, StoreList 11 | 12 | app = Flask(__name__) 13 | 14 | app.config['DEBUG'] = True 15 | 16 | app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///data.db') 17 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 18 | app.config['PROPAGATE_EXCEPTIONS'] = True 19 | app.secret_key = 'jose' 20 | api = Api(app) 21 | 22 | jwt = JWT(app, authenticate, identity) # /auth 23 | 24 | api.add_resource(Store, '/store/') 25 | api.add_resource(Item, '/item/') 26 | api.add_resource(ItemList, '/items') 27 | api.add_resource(StoreList, '/stores') 28 | 29 | api.add_resource(UserRegister, '/register') 30 | 31 | 32 | @app.errorhandler(JWTError) 33 | def auth_error(err): 34 | return jsonify({'message': 'Could not authorize. Did you include a valid Authorization header?'}), 401 35 | 36 | 37 | if __name__ == '__main__': 38 | from db import db 39 | 40 | db.init_app(app) 41 | 42 | if app.config['DEBUG']: 43 | @app.before_first_request 44 | def create_tables(): 45 | db.create_all() 46 | 47 | app.run(port=5000) 48 | -------------------------------------------------------------------------------- /section8/video_code/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /section8/video_code/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/models/__init__.py -------------------------------------------------------------------------------- /section8/video_code/models/item.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class ItemModel(db.Model): 5 | __tablename__ = 'items' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | price = db.Column(db.Float(precision=2)) 10 | 11 | store_id = db.Column(db.Integer, db.ForeignKey('stores.id')) 12 | store = db.relationship('StoreModel', back_populates="items") 13 | 14 | def __init__(self, name, price, store_id): 15 | self.name = name 16 | self.price = price 17 | self.store_id = store_id 18 | 19 | def json(self): 20 | return {'name': self.name, 'price': self.price} 21 | 22 | @classmethod 23 | def find_by_name(cls, name): 24 | return cls.query.filter_by(name=name).first() 25 | 26 | def save_to_db(self): 27 | db.session.add(self) 28 | db.session.commit() 29 | 30 | def delete_from_db(self): 31 | db.session.delete(self) 32 | db.session.commit() 33 | -------------------------------------------------------------------------------- /section8/video_code/models/store.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class StoreModel(db.Model): 5 | __tablename__ = 'stores' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80)) 9 | 10 | items = db.relationship('ItemModel', lazy='dynamic', back_populates="store") 11 | 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | def json(self): 16 | return {'name': self.name, 'items': [item.json() for item in self.items.all()]} 17 | 18 | @classmethod 19 | def find_by_name(cls, name): 20 | return cls.query.filter_by(name=name).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def delete_from_db(self): 27 | db.session.delete(self) 28 | db.session.commit() 29 | -------------------------------------------------------------------------------- /section8/video_code/models/user.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | 3 | 4 | class UserModel(db.Model): 5 | __tablename__ = 'users' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | username = db.Column(db.String(80)) 9 | password = db.Column(db.String(80)) 10 | 11 | def __init__(self, username, password): 12 | self.username = username 13 | self.password = password 14 | 15 | def save_to_db(self): 16 | db.session.add(self) 17 | db.session.commit() 18 | 19 | @classmethod 20 | def find_by_username(cls, username): 21 | return cls.query.filter_by(username=username).first() 22 | 23 | @classmethod 24 | def find_by_id(cls, _id): 25 | return cls.query.filter_by(id=_id).first() 26 | -------------------------------------------------------------------------------- /section8/video_code/readme.md: -------------------------------------------------------------------------------- 1 | # Stores REST Api 2 | 3 | This is built with Flask, Flask-RESTful, Flask-JWT, and Flask-SQLAlchemy. 4 | 5 | Deployed on Heroku. 6 | -------------------------------------------------------------------------------- /section8/video_code/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-JWT 4 | Flask-SQLAlchemy 5 | uwsgi 6 | psycopg2 7 | -------------------------------------------------------------------------------- /section8/video_code/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/resources/__init__.py -------------------------------------------------------------------------------- /section8/video_code/resources/item.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from flask_jwt import jwt_required 3 | from models.item import ItemModel 4 | 5 | 6 | class Item(Resource): 7 | parser = reqparse.RequestParser() 8 | parser.add_argument('price', 9 | type=float, 10 | required=True, 11 | help="This field cannot be left blank!") 12 | parser.add_argument('store_id', 13 | type=int, 14 | required=True, 15 | help="Every item needs a store id.") 16 | 17 | @jwt_required() 18 | def get(self, name): 19 | item = ItemModel.find_by_name(name) 20 | if item: 21 | return item.json() 22 | return {'message': 'Item not found'}, 404 23 | 24 | def post(self, name): 25 | if ItemModel.find_by_name(name): 26 | return {'message': "An item with name '{}' already exists.".format(name)}, 400 27 | 28 | data = Item.parser.parse_args() 29 | 30 | item = ItemModel(name, **data) 31 | 32 | try: 33 | item.save_to_db() 34 | except: 35 | return {"message": "An error occurred inserting the item."}, 500 36 | 37 | return item.json(), 201 38 | 39 | def delete(self, name): 40 | item = ItemModel.find_by_name(name) 41 | if item: 42 | item.delete_from_db() 43 | 44 | return {'message': 'Item deleted'} 45 | 46 | def put(self, name): 47 | data = Item.parser.parse_args() 48 | 49 | item = ItemModel.find_by_name(name) 50 | 51 | if item is None: 52 | item = ItemModel(name, **data) 53 | else: 54 | item.price = data['price'] 55 | 56 | item.save_to_db() 57 | 58 | return item.json() 59 | 60 | 61 | class ItemList(Resource): 62 | def get(self): 63 | return {'items': [x.json() for x in ItemModel.query.all()]} 64 | -------------------------------------------------------------------------------- /section8/video_code/resources/store.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from models.store import StoreModel 3 | 4 | 5 | class Store(Resource): 6 | def get(self, name): 7 | store = StoreModel.find_by_name(name) 8 | if store: 9 | return store.json() 10 | return {'message': 'Store not found'}, 404 11 | 12 | def post(self, name): 13 | if StoreModel.find_by_name(name): 14 | return {'message': "A store with name '{}' already exists.".format(name)}, 400 15 | 16 | store = StoreModel(name) 17 | try: 18 | store.save_to_db() 19 | except: 20 | return {"message": "An error occurred creating the store."}, 500 21 | 22 | return store.json(), 201 23 | 24 | def delete(self, name): 25 | store = StoreModel.find_by_name(name) 26 | if store: 27 | store.delete_from_db() 28 | 29 | return {'message': 'Store deleted'} 30 | 31 | 32 | class StoreList(Resource): 33 | def get(self): 34 | return {'stores': [store.json() for store in StoreModel.query.all()]} 35 | -------------------------------------------------------------------------------- /section8/video_code/resources/user.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from models.user import UserModel 3 | 4 | 5 | class UserRegister(Resource): 6 | parser = reqparse.RequestParser() 7 | parser.add_argument('username', 8 | type=str, 9 | required=True, 10 | help="This field cannot be blank.") 11 | parser.add_argument('password', 12 | type=str, 13 | required=True, 14 | help="This field cannot be blank.") 15 | 16 | def post(self): 17 | data = UserRegister.parser.parse_args() 18 | 19 | if UserModel.find_by_username(data['username']): 20 | return {"message": "A user with that username already exists"}, 400 21 | 22 | user = UserModel(**data) 23 | user.save_to_db() 24 | 25 | return {"message": "User created successfully."}, 201 26 | -------------------------------------------------------------------------------- /section8/video_code/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | 4 | db.init_app(app) 5 | 6 | 7 | @app.before_first_request 8 | def create_tables(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /section8/video_code/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.2 2 | -------------------------------------------------------------------------------- /section8/video_code/security.py: -------------------------------------------------------------------------------- 1 | from hmac import compare_digest 2 | from models.user import UserModel 3 | 4 | 5 | def authenticate(username, password): 6 | user = UserModel.find_by_username(username) 7 | if user and compare_digest(user.password, password): 8 | return user 9 | 10 | 11 | def identity(payload): 12 | user_id = payload['identity'] 13 | return UserModel.find_by_id(user_id) 14 | -------------------------------------------------------------------------------- /section8/video_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/tests/__init__.py -------------------------------------------------------------------------------- /section8/video_code/tests/base_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | BaseTest 3 | 4 | This class should be the parent class to each unit test. 5 | It allows for instantiation of the database dynamically, 6 | and makes sure that it is a new, blank database each time. 7 | """ 8 | 9 | from unittest import TestCase 10 | from app import app 11 | from db import db 12 | 13 | 14 | class BaseTest(TestCase): 15 | SQLALCHEMY_DATABASE_URI = "sqlite://" 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | app.config['SQLALCHEMY_DATABASE_URI'] = BaseTest.SQLALCHEMY_DATABASE_URI 20 | app.config['DEBUG'] = False 21 | with app.app_context(): 22 | db.init_app(app) 23 | 24 | def setUp(self): 25 | with app.app_context(): 26 | db.create_all() 27 | self.app = app.test_client 28 | self.app_context = app.app_context 29 | 30 | def tearDown(self): 31 | with app.app_context(): 32 | db.session.remove() 33 | db.drop_all() 34 | -------------------------------------------------------------------------------- /section8/video_code/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/tests/integration/__init__.py -------------------------------------------------------------------------------- /section8/video_code/tests/integration/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/tests/integration/models/__init__.py -------------------------------------------------------------------------------- /section8/video_code/tests/integration/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from models.store import StoreModel 3 | from tests.base_test import BaseTest 4 | 5 | 6 | class ItemTest(BaseTest): 7 | def test_crud(self): 8 | with self.app_context(): 9 | store = StoreModel('test') 10 | store.save_to_db() 11 | item = ItemModel('test', 19.99, 1) 12 | 13 | self.assertIsNone(ItemModel.find_by_name('test'), "Found an item with name 'test' before save_to_db") 14 | 15 | item.save_to_db() 16 | 17 | self.assertIsNotNone(ItemModel.find_by_name('test'), 18 | "Did not find an item with name 'test' after save_to_db") 19 | 20 | item.delete_from_db() 21 | 22 | self.assertIsNone(ItemModel.find_by_name('test'), "Found an item with name 'test' after delete_from_db") 23 | 24 | def test_store_relationship(self): 25 | with self.app_context(): 26 | store = StoreModel('test_store') 27 | item = ItemModel('test', 19.99, 1) 28 | 29 | store.save_to_db() 30 | item.save_to_db() 31 | 32 | self.assertEqual(item.store.name, 'test_store') 33 | -------------------------------------------------------------------------------- /section8/video_code/tests/integration/models/store_test.py: -------------------------------------------------------------------------------- 1 | from models.store import StoreModel 2 | from models.item import ItemModel 3 | from tests.base_test import BaseTest 4 | 5 | 6 | class StoreTest(BaseTest): 7 | def test_crud(self): 8 | with self.app_context(): 9 | store = StoreModel('test') 10 | 11 | self.assertIsNone(StoreModel.find_by_name('test'), "Found an store with name 'test' before save_to_db") 12 | 13 | store.save_to_db() 14 | 15 | self.assertIsNotNone(StoreModel.find_by_name('test'), 16 | "Did not find an store with name 'test' after save_to_db") 17 | 18 | store.delete_from_db() 19 | 20 | self.assertIsNone(StoreModel.find_by_name('test'), "Found an store with name 'test' after delete_from_db") 21 | 22 | def test_store_relationship(self): 23 | with self.app_context(): 24 | store = StoreModel('test') 25 | item = ItemModel('test_item', 19.99, 1) 26 | 27 | store.save_to_db() 28 | item.save_to_db() 29 | 30 | self.assertEqual(store.items.count(), 1) 31 | self.assertEqual(store.items.first().name, 'test_item') 32 | -------------------------------------------------------------------------------- /section8/video_code/tests/integration/models/user_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class UserTest(BaseTest): 6 | def test_crud(self): 7 | with self.app_context(): 8 | user = UserModel('test', 'abcd') 9 | 10 | self.assertIsNone(UserModel.find_by_username('test'), "Found an user with name 'test' before save_to_db") 11 | self.assertIsNone(UserModel.find_by_id(1), "Found an user with id '1' before save_to_db") 12 | 13 | user.save_to_db() 14 | 15 | self.assertIsNotNone(UserModel.find_by_username('test'), 16 | "Did not find an user with name 'test' after save_to_db") 17 | self.assertIsNotNone(UserModel.find_by_id(1), "Did not find an user with id '1' after save_to_db") 18 | -------------------------------------------------------------------------------- /section8/video_code/tests/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/tests/system/__init__.py -------------------------------------------------------------------------------- /section8/video_code/tests/system/item_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from models.item import ItemModel 3 | from models.store import StoreModel 4 | from tests.base_test import BaseTest 5 | import json 6 | 7 | 8 | class ItemTest(BaseTest): 9 | def setUp(self): 10 | super(ItemTest, self).setUp() 11 | with self.app() as c: 12 | with self.app_context(): 13 | UserModel('test', '1234').save_to_db() 14 | auth_request = c.post('/auth', data=json.dumps({ 15 | 'username': 'test', 16 | 'password': '1234' 17 | }), headers={'Content-Type': 'application/json'}) 18 | self.auth_header = "JWT {}".format(json.loads(auth_request.data)['access_token']) 19 | 20 | def test_item_no_auth(self): 21 | with self.app() as c: 22 | r = c.get('/item/test') 23 | self.assertEqual(r.status_code, 401) 24 | 25 | def test_item_not_found(self): 26 | with self.app() as c: 27 | r = c.get('/item/test', headers={'Authorization': self.auth_header}) 28 | self.assertEqual(r.status_code, 404) 29 | 30 | def test_item_found(self): 31 | with self.app() as c: 32 | with self.app_context(): 33 | StoreModel('test').save_to_db() 34 | ItemModel('test', 17.99, 1).save_to_db() 35 | r = c.get('/item/test', headers={'Authorization': self.auth_header}) 36 | 37 | self.assertEqual(r.status_code, 200) 38 | self.assertDictEqual(d1={'name': 'test', 'price': 17.99}, 39 | d2=json.loads(r.data)) 40 | 41 | def test_delete_item(self): 42 | with self.app() as c: 43 | with self.app_context(): 44 | StoreModel('test').save_to_db() 45 | ItemModel('test', 17.99, 1).save_to_db() 46 | r = c.delete('/item/test') 47 | 48 | self.assertEqual(r.status_code, 200) 49 | self.assertDictEqual(d1={'message': 'Item deleted'}, 50 | d2=json.loads(r.data)) 51 | 52 | def test_create_item(self): 53 | with self.app() as c: 54 | with self.app_context(): 55 | StoreModel('test').save_to_db() 56 | r = c.post('/item/test', data={'price': 17.99, 'store_id': 1}) 57 | 58 | self.assertEqual(r.status_code, 201) 59 | self.assertEqual(ItemModel.find_by_name('test').price, 17.99) 60 | self.assertDictEqual(d1={'name': 'test', 'price': 17.99}, 61 | d2=json.loads(r.data)) 62 | 63 | def test_create_duplicate_item(self): 64 | with self.app() as c: 65 | with self.app_context(): 66 | StoreModel('test').save_to_db() 67 | c.post('/item/test', data={'price': 17.99, 'store_id': 1}) 68 | r = c.post('/item/test', data={'price': 17.99, 'store_id': 1}) 69 | 70 | self.assertEqual(r.status_code, 400) 71 | 72 | def test_put_item(self): 73 | with self.app() as c: 74 | with self.app_context(): 75 | StoreModel('test').save_to_db() 76 | r = c.put('/item/test', data={'price': 17.99, 'store_id': 1}) 77 | 78 | self.assertEqual(r.status_code, 200) 79 | self.assertEqual(ItemModel.find_by_name('test').price, 17.99) 80 | self.assertDictEqual(d1={'name': 'test', 'price': 17.99}, 81 | d2=json.loads(r.data)) 82 | 83 | def test_put_update_item(self): 84 | with self.app() as c: 85 | with self.app_context(): 86 | StoreModel('test').save_to_db() 87 | c.put('/item/test', data={'price': 17.99, 'store_id': 1}) 88 | r = c.put('/item/test', data={'price': 18.99, 'store_id': 1}) 89 | 90 | self.assertEqual(r.status_code, 200) 91 | self.assertEqual(ItemModel.find_by_name('test').price, 18.99) 92 | 93 | def test_item_list(self): 94 | with self.app() as c: 95 | with self.app_context(): 96 | StoreModel('test').save_to_db() 97 | ItemModel('test', 17.99, 1).save_to_db() 98 | r = c.get('/items') 99 | 100 | self.assertDictEqual(d1={'items': [{'name': 'test', 'price': 17.99}]}, 101 | d2=json.loads(r.data)) 102 | -------------------------------------------------------------------------------- /section8/video_code/tests/system/store_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from models.store import StoreModel 3 | from tests.base_test import BaseTest 4 | import json 5 | 6 | 7 | class StoreTest(BaseTest): 8 | def test_store_not_found(self): 9 | with self.app() as c: 10 | r = c.get('/store/test') 11 | self.assertEqual(r.status_code, 404) 12 | 13 | def test_store_found(self): 14 | with self.app() as c: 15 | with self.app_context(): 16 | StoreModel('test').save_to_db() 17 | r = c.get('/store/test') 18 | 19 | self.assertEqual(r.status_code, 200) 20 | self.assertDictEqual(d1={'name': 'test', 'items': []}, 21 | d2=json.loads(r.data)) 22 | 23 | def test_store_with_items_found(self): 24 | with self.app() as c: 25 | with self.app_context(): 26 | StoreModel('test').save_to_db() 27 | ItemModel('test', 2.99, 1).save_to_db() 28 | r = c.get('/store/test') 29 | 30 | self.assertEqual(r.status_code, 200) 31 | self.assertDictEqual(d1={'name': 'test', 'items': [{'name': 'test', 'price': 2.99}]}, 32 | d2=json.loads(r.data)) 33 | 34 | def test_delete_store(self): 35 | with self.app() as c: 36 | with self.app_context(): 37 | StoreModel('test').save_to_db() 38 | r = c.delete('/store/test') 39 | 40 | self.assertEqual(r.status_code, 200) 41 | self.assertDictEqual(d1={'message': 'Store deleted'}, 42 | d2=json.loads(r.data)) 43 | 44 | def test_create_store(self): 45 | with self.app() as c: 46 | with self.app_context(): 47 | r = c.post('/store/test') 48 | 49 | self.assertEqual(r.status_code, 201) 50 | self.assertIsNotNone(StoreModel.find_by_name('test')) 51 | self.assertDictEqual(d1={'name': 'test', 'items': []}, 52 | d2=json.loads(r.data)) 53 | 54 | def test_create_duplicate_store(self): 55 | with self.app() as c: 56 | with self.app_context(): 57 | c.post('/store/test') 58 | r = c.post('/store/test') 59 | 60 | self.assertEqual(r.status_code, 400) 61 | 62 | def test_store_list(self): 63 | with self.app() as c: 64 | with self.app_context(): 65 | StoreModel('test').save_to_db() 66 | r = c.get('/stores') 67 | 68 | self.assertDictEqual(d1={'stores': [{'name': 'test', 'items': []}]}, 69 | d2=json.loads(r.data)) 70 | 71 | def test_store_with_items_list(self): 72 | with self.app() as c: 73 | with self.app_context(): 74 | StoreModel('test').save_to_db() 75 | ItemModel('test', 17.99, 1).save_to_db() 76 | r = c.get('/stores') 77 | 78 | self.assertDictEqual(d1={'stores': [{'name': 'test', 'items': [{'name': 'test', 'price': 17.99}]}]}, 79 | d2=json.loads(r.data)) 80 | -------------------------------------------------------------------------------- /section8/video_code/tests/system/user_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from tests.base_test import BaseTest 3 | import json 4 | 5 | 6 | class UserTest(BaseTest): 7 | def test_register_user(self): 8 | with self.app() as c: 9 | with self.app_context(): 10 | r = c.post('/register', data={'username': 'test', 'password': '1234'}) 11 | 12 | self.assertEqual(r.status_code, 201) 13 | self.assertIsNotNone(UserModel.find_by_username('test')) 14 | self.assertDictEqual(d1={'message': 'User created successfully.'}, 15 | d2=json.loads(r.data)) 16 | 17 | def test_register_and_login(self): 18 | with self.app() as c: 19 | with self.app_context(): 20 | c.post('/register', data={'username': 'test', 'password': '1234'}) 21 | auth_request = c.post('/auth', data=json.dumps({ 22 | 'username': 'test', 23 | 'password': '1234' 24 | }), headers={'Content-Type': 'application/json'}) 25 | 26 | self.assertIn('access_token', json.loads(auth_request.data).keys()) 27 | 28 | def test_register_duplicate_user(self): 29 | with self.app() as c: 30 | with self.app_context(): 31 | c.post('/register', data={'username': 'test', 'password': '1234'}) 32 | r = c.post('/register', data={'username': 'test', 'password': '1234'}) 33 | 34 | self.assertEqual(r.status_code, 400) 35 | self.assertDictEqual(d1={'message': 'A user with that username already exists'}, 36 | d2=json.loads(r.data)) 37 | -------------------------------------------------------------------------------- /section8/video_code/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/tests/unit/__init__.py -------------------------------------------------------------------------------- /section8/video_code/tests/unit/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecladocode/testing-python-apps/e9cbacb9fd2f8660856ec60ace5db015aa93ea2f/section8/video_code/tests/unit/models/__init__.py -------------------------------------------------------------------------------- /section8/video_code/tests/unit/models/item_test.py: -------------------------------------------------------------------------------- 1 | from models.item import ItemModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class ItemTest(BaseTest): 6 | def test_create_item(self): 7 | item = ItemModel('test', 19.99, 1) 8 | 9 | self.assertEqual(item.name, 'test', 10 | "The name of the item after creation does not equal the constructor argument.") 11 | self.assertEqual(item.price, 19.99, 12 | "The price of the item after creation does not equal the constructor argument.") 13 | self.assertEqual(item.store_id, 1, 14 | "The store_id of the item after creation does not equal the constructor argument.") 15 | self.assertIsNone(item.store, "The item's store was not None even though the store was not created.") 16 | 17 | def test_item_json(self): 18 | item = ItemModel('test', 19.99, 1) 19 | expected = { 20 | 'name': 'test', 21 | 'price': 19.99 22 | } 23 | 24 | self.assertEqual( 25 | item.json(), 26 | expected, 27 | "The JSON export of the item is incorrect. Received {}, expected {}.".format(item.json(), expected)) 28 | -------------------------------------------------------------------------------- /section8/video_code/tests/unit/models/store_test.py: -------------------------------------------------------------------------------- 1 | from models.store import StoreModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class StoreTest(BaseTest): 6 | def test_create_store(self): 7 | store = StoreModel('test') 8 | 9 | self.assertEqual(store.name, 'test', 10 | "The name of the store after creation does not equal the constructor argument.") 11 | self.assertListEqual(store.items.all(), [], 12 | "The store's items length was not 0 even though no items were added.") 13 | 14 | def test_store_json(self): 15 | store = StoreModel('test') 16 | expected = { 17 | 'name': 'test', 18 | 'items': [] 19 | } 20 | 21 | self.assertEqual( 22 | store.json(), 23 | expected, 24 | "The JSON export of the store is incorrect. Received {}, expected {}.".format(store.json(), expected)) 25 | -------------------------------------------------------------------------------- /section8/video_code/tests/unit/models/user_test.py: -------------------------------------------------------------------------------- 1 | from models.user import UserModel 2 | from tests.base_test import BaseTest 3 | 4 | 5 | class UserTest(BaseTest): 6 | def test_create_user(self): 7 | user = UserModel('test', 'abcd') 8 | 9 | self.assertEqual(user.username, 'test', 10 | "The name of the user after creation does not equal the constructor argument.") 11 | self.assertEqual(user.password, 'abcd', 12 | "The password of the user after creation does not equal the constructor argument.") 13 | -------------------------------------------------------------------------------- /section8/video_code/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | die-on-term = true 5 | module = run:app 6 | memory-report = true 7 | -------------------------------------------------------------------------------- /testing-python-apps.md: -------------------------------------------------------------------------------- 1 | # Testing Python Apps 2 | 3 | ## Building Python apps 4 | 5 | ## Testing while you build 6 | 7 | ### Section 3: Intro to unittest 8 | 9 | ### Section 4: Unit testing a simple Flask endpoint 10 | 11 | ### Section 5: Unit testing a REST API 12 | 13 | #### Continuous Integration 14 | 15 | - Travis CI is set up only for Section 2. 16 | 17 | https://docs.travis-ci.com/user/languages/python/ 18 | 19 | ### Section 6: Integration testing a REST API 20 | 21 | ### Sections 7 + 8: System testing a REST API 22 | 23 | #### 7: unittest 24 | 25 | #### 8: Postman + newman 26 | 27 | Remember to cover installing the necessary PyCharm plugins to be able to run things 28 | simultaneously. Need: 29 | 30 | - `Multirun`, to allow to run multiple configurations in parallel. 31 | - `BashSupport` (or `CMDSupport` on Windows), to allow running command-line scripts. 32 | - Also need to check how to do this on Windows 33 | 34 | ### Section 9: Acceptance testing a website 35 | 36 | ### Section 10: Contract testing (not now) --------------------------------------------------------------------------------