├── .gitignore ├── README.md ├── kino_salon.txt ├── module01 ├── README.md └── test.py ├── module02 ├── README.md └── test.py ├── module03 ├── README.md └── test.py ├── module04 ├── 01-Cash-Desk │ ├── README.md │ └── test.py ├── 02-Bank-Account │ ├── README.md │ └── test.py ├── 03-Panda-Social-Network │ ├── README.md │ └── test.py └── README.md ├── module05 ├── README.md ├── calculator.py ├── test.py └── test_example.py ├── module06 ├── README.md └── test.py ├── module07 ├── README.md └── examples │ ├── pages.py │ └── test.py ├── module08 └── README.md └── workshop └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | solution.py* 2 | *.pyc 3 | .idea/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QA automation with Python & Selenium 101 2 | 3 | ## Module 00 - Environment setup 4 | 5 | Students need to prepare the following environment on their computers: 6 | 7 | * Install Python 3 on your computer from https://www.python.org/downloads/; 8 | * Text editor of choice, preferably [PyCharm IDE](https://www.jetbrains.com/pycharm/); 9 | * Install & configure source control system of choice (or git). 10 | 11 | **NOTES:** 12 | 13 | * Students should be prepared before the first Module since the 14 | training does not intend to focus on installation related issues! 15 | * Students must be familiar with their tools of choice, e.g. they need to know 16 | how to use the IDE or text editor, how to search files, how to search for 17 | particular text, etc; 18 | * Instructor may be using different text editor and operating system and may not be 19 | able to help with matters related to any particular tool if they are not obvious 20 | enough; 21 | * Students are advised to use the same tools so they can help each other; 22 | Preferably the same tools are used inside the company as well; 23 | * Student must read all preparation materials before class begins; 24 | * Each Module begins with review of homework from previous Module; 25 | * Classes will explain the theory from the preparation section with examples 26 | and focus on writing programs to solidify the knowledge; 27 | * Each module is scheduled to take 1 week (total of 3 hrs) of onsite training; 28 | * All homework needs to be done by COB Friday so that Instructor can review and 29 | provide feedback on it! 30 | 31 | Useful links: 32 | 33 | * [Dive into Python](http://www.diveintopython.net/) 34 | * [The Python Tutorial](https://docs.python.org/3/tutorial/index.html) 35 | * [Official Python documentation](https://docs.python.org/) 36 | * [Unofficial Selenium Python documentation](http://selenium-python.readthedocs.io/) 37 | * [Official Selenium API documentation for Python](https://seleniumhq.github.io/selenium/docs/api/py/api.html) 38 | 39 | 40 | ## Module 01: Structure of a Python program 41 | ## Module 02: Data types and structures 42 | ## Module 03: If statements and loops 43 | ## Module 04: Classes and objects 44 | ## Module 05: Unit testing 45 | ## Module 06: Selenium with Python 46 | ## Module 07: Page Objects design pattern 47 | ## Module 08: Writing automated tests for real scenarios 48 | 49 | All materials here are licensed under CC-BY-SA license, see 50 | https://creativecommons.org/licenses/by-sa/4.0/ for more information. 51 | -------------------------------------------------------------------------------- /kino_salon.txt: -------------------------------------------------------------------------------- 1 | Кино Салон 2 | ---------- 3 | 4 | - Програмата да може да инициализира база данни със филми, прожекции (за текущата седмица) 5 | и цени на билетите от сайтовете на Кино Арена или Синема Сити. 6 | 7 | - Да може да се избере данните от кое кино да бъдат импортирани в програмата 8 | 9 | - Информацията трябва да се запазва в SQLite база данни 10 | 11 | - Имената на филмите на кирилица трябва да се запазят 12 | 13 | - Съществува функционалност за закупуване на билети през cli 14 | 15 | - Програмата може да генерира следните статистики: 16 | - ТОП 3 на филмите за седмицата (по брой посещения) 17 | - ТОП 3 по дни 18 | - Общо приходи за седмицата 19 | - Филми с най-голям и най-малък приход 20 | - Приходи по часови зони от по 3 часа 21 | 22 | - Трябва да имаме експорт на финансовите справки към CSV, за да 23 | можем да ги изпратим на счетводния отдел 24 | 25 | - Трябва да имаме backup/restore на БД към/от JSON файл 26 | 27 | - Програмата трябва да има покритие с unit test 28 | 29 | - Трябва да се използват collections, map/filter, lambda където е подходящо 30 | 31 | - Математическите операции за справките да може да се разпишат като x + y, 32 | т.е. кратко, където това е подходящо. 33 | 34 | - JSON формата трябва да е "стандартен", т.е. различни имплементации 35 | на програмата да могат да обменят данни. Този "стандарт" трябва да се постигне 36 | с дискусия, след като всеки е избрал структура на БД. 37 | -------------------------------------------------------------------------------- /module01/README.md: -------------------------------------------------------------------------------- 1 | # Module 01: Structure of a Python program 2 | 3 | ## Preparation 4 | 5 | * Read chapter 02 from Dive into Python! 6 | 7 | ## Starting the Python interpreter 8 | 9 | An interactive session (useful for trying out things) can be started from the 10 | terminal by typing: 11 | 12 | $ python 13 | Python 3.5.1 (default, Sep 15 2016, 08:30:32) 14 | [GCC 4.8.3 20140911 (Red Hat 4.8.3-9)] on linux 15 | Type "help", "copyright", "credits" or "license" for more information. 16 | >>> print("Hello World") 17 | Hello World 18 | >>> 19 | 20 | To exit the interactive interpreter type `exit()` or press Ctrl+D. 21 | 22 | ## Starting Python programs 23 | 24 | Any Python program can be started by passing the file name as argument to the 25 | Python intepreter. In the terminal type: 26 | 27 | $ python myprogram.py 28 | 29 | ## Program structure 30 | 31 | Each Python program consists of several basic blocks: 32 | 33 | - module imports 34 | - statements 35 | - function definitions 36 | - class definitions 37 | - an optional main block! 38 | 39 | For example: 40 | 41 | import os 42 | 43 | name = 'Alex' 44 | 45 | def sayHello(who): 46 | print "Hello %s" % who 47 | 48 | class Person(object): 49 | pass 50 | 51 | if __name__ == "__main__": 52 | sayHello(name) 53 | sayHello('Maria') 54 | 55 | 56 | Program blocks are recognized by their indentation level. There are no `begin` or `end` 57 | keywords. In Python we use 4 spaces for indentation! 58 | 59 | ## Variables 60 | 61 | Variables are used to store data in the program. You can assign values to variables by 62 | using the assignment operator (`=`) like so: 63 | 64 | name = 'Ivan' 65 | print('Hello', name) 66 | 67 | name = 'Alex' 68 | print('Good afternoon', name) 69 | 70 | 71 | Try this program in the interactive interpreter! 72 | 73 | 74 | ## Functions 75 | 76 | Functions are sequence of operations which can be applied multiple times 77 | onto different arguments. For example a function that can send email 78 | will perform the exact same operations every time but the message and the 79 | recipient can be controlled via arguments. 80 | 81 | Functions are defined as follows: 82 | 83 | def functionName([list of parameters]): 84 | """ 85 | Doc-string documenting what this function does. 86 | """ 87 | 88 | return 89 | 90 | The function name and list of parameters is called a function signature! 91 | 92 | Functions return result via the `return ` statement. When `return` 93 | is executed the function execution completes immediately. If no `return` 94 | statement is specified then the default return value is `None`! 95 | 96 | For example a function to calculate the perimeter of a square will look like this: 97 | 98 | def perimeter_of_square(side): 99 | return 4 * side 100 | 101 | A function to calculate perimeter of triangle will look like this: 102 | 103 | def perimeter_of_triangle(a, b, c): 104 | return a + b + c 105 | 106 | 107 | In the example above the variables `a`, `b` and `c` are called parameters. Parameter 108 | variables are accessible everywhere inside the function body. When we want to perform 109 | a calculation for a particular triangle then we call(execute) the function like this: 110 | 111 | >>> perimeter_of_triangle(1, 2, 3) 112 | 6 113 | >>> perimeter_of_triangle(2, 4, 6) 114 | 12 115 | 116 | The values `1`, `2`, `3` are called arguments! They are assigned to the parameter variables 117 | when the function is executed. 118 | 119 | 120 | 121 | ## Comments and doc-strings 122 | 123 | Everything after a `#` (hash sign) is a comment and is ignored by the Python interpreter! 124 | After defining a function, module or class you can use a triple quoted string to provide 125 | description of what that object does. This is called the doc-string of the object and is 126 | used by the integrated help system in Python. For example: 127 | 128 | >>> import os 129 | >>> help(os) 130 | >>> # ^^^ press q to quit 131 | >>> 132 | >>> def perimeter_of_triangle(a, b, c): 133 | ... '''Calculates the primeter of a triangle''' 134 | ... return a + b + c 135 | ... 136 | >>> help(perimeter_of_triangle) 137 | 138 | Help on function perimeter_of_triangle in module __main__: 139 | 140 | perimeter_of_triangle(a, b, c) 141 | Calculates the primeter of a triangle 142 | (END) 143 | 144 | 145 | ## Modules and imports 146 | 147 | Shared functionality is defined inside Python modules. A module may be: 148 | 149 | - a `file_name.py` or 150 | - a directory with an `__init__.py` file inside 151 | 152 | Modules are loaded and used into the program via: 153 | 154 | import mymodule 155 | from another_module import sayHello 156 | 157 | mymodule.whatTimeIsIt() 158 | sayHello('Alex') 159 | 160 | After a module has been imported you can read its documentation with 161 | 162 | help(mymodule) 163 | 164 | All modules from the Python standard library are documented at https://docs.python.org 165 | 166 | The module search path is defined in the `sys.path` variable! This is a list of 167 | directories in which modules are searched (from first to last). It can also be used 168 | to alter the search path to include non-standard directories! 169 | 170 | 171 | ## Main block 172 | 173 | A program may have an optional main block. It is defined as an if statement at the 174 | top-most level: 175 | 176 | if __name__ == "__main__": 177 | 178 | 179 | Main blocks are executed only if the file is executed as a program, 180 | e.g. `python myfile.py`. If the file is loaded as a module, e.g. `import myfile` 181 | the main block is not executed. 182 | 183 | The main block is optional. You can execute functions and assign to variables 184 | at the top-most level as well. However this will have undesired effects if 185 | your files are imported as modules! 186 | 187 | 188 | ## Tasks & homework 189 | 190 | * Create a program named `solution.py` 191 | * Define a function with the following signature `def f_c(X)` which 192 | returns the constant 4 for any input parameter. 193 | * Document what the function `f_c` does 194 | * Write a function `f_x(x, a, b)` which implements the formula `f(x) = a*x + b`! 195 | * Write a function `sum(x)` which returns the sum of `f_x()` called 3 times with 196 | parameters `x, 1, 1`, `x, 2, 2`, `x, 3, 3`! 197 | 198 | **TIP:** Use `test.py` to validate your solution is correct. 199 | 200 | -------------------------------------------------------------------------------- /module01/test.py: -------------------------------------------------------------------------------- 1 | import solution 2 | import unittest 3 | 4 | class TestSolution(unittest.TestCase): 5 | def test_f_c(self): 6 | self.assertTrue(solution.f_c.__doc__) 7 | 8 | for i in range(-10, 10): 9 | self.assertEqual(4, solution.f_c(i)) 10 | 11 | def test_f_x(self): 12 | for x in range(-10, 10): 13 | for a in range(-10, 10): 14 | for b in range(-10, 10): 15 | self.assertEqual(a*x + b, solution.f_x(x, a, b)) 16 | 17 | def test_sum(self): 18 | for x in range(-10, 10): 19 | expected = 0 20 | for i in range(1, 4): 21 | expected += solution.f_x(x, i, i) 22 | 23 | self.assertEqual(expected, solution.sum(x)) 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | 29 | -------------------------------------------------------------------------------- /module02/README.md: -------------------------------------------------------------------------------- 1 | # Module 02: Data types and data structures 2 | 3 | ## Preparation 4 | 5 | * Read chapter 03 from Dive into Python; 6 | * Read chapter [Built-in types](https://docs.python.org/3/library/stdtypes.html) 7 | from Python's documentation. 8 | 9 | ## Main data types 10 | 11 | Data types represent values with common properties. For example `int` represents all 12 | integer numbers. You can perform comparison and mathematical operations on `int` numbers. 13 | `str` is the type representing text values. 14 | 15 | In Python the main data types are `int`, `float`, `boolean`, `str`, `list`, `dict`, 16 | `tuple` and `set`! Operators for these types are defined in the documentation and will 17 | be used in the tasks below. We will explain them as we go along! 18 | 19 | 20 | ## DateTime types 21 | 22 | Python has special types for working with dates and times. They live in the 23 | [datetime module](https://docs.python.org/3/library/datetime.html). 24 | 25 | >>> from datetime import datetime, timedelta 26 | >>> datetime.now() 27 | datetime.datetime(2017, 8, 5, 23, 43, 50, 500589) 28 | >>> datetime.now().isoformat() 29 | '2017-08-05T23:44:02.508418' 30 | >>> datetime.now().year 31 | 2017 32 | >>> datetime.now() - timedelta(days=7) 33 | datetime.datetime(2017, 7, 29, 23, 44, 33, 101454) 34 | 35 | 36 | ## Tasks & homework 37 | 38 | * Implement a function `num_add(a, b)` which adds two numbers together 39 | * Implement a function `num_sub(a, b)` which subtracts two numbers 40 | * Implement a function `num_mul(a, b)` which multiplies two numbers 41 | * Implement a function `num_div(a, b)` which divides the two numbers 42 | * Implement a function `num_floor(a, b)` which implements floor division 43 | * Implement a function `num_rem(a, b)` which implements remainder division 44 | * Define boolean constant `IS_TRUE` 45 | * Define boolean constant `IS_FALSE` 46 | * Define the `PANCAKE_INGREDIENTS` dictionary to include the following keys and values 47 | 48 | flour - 2 49 | eggs - 4 50 | milk - 200 51 | butter - False 52 | salt - 0.001 53 | 54 | * Implement a function `ingredient_exists(ingr, dict)` which returns boolean if the 55 | ingredient `ingr` exists in the dictionary `dict 56 | * Implement a function `fatten_pancakes(dict)` which returns a dictionary. The return 57 | value contains the pancake ingredients where `eggs == 6` and `butter == True`. 58 | **NOTE:** don't change the `PANCAKE_INGREDIENTS` constant! Use `dict.copy()` method! 59 | * Implement a function `add_sugar(dict)` which adds 'sugar' to the list of ingredients 60 | and returns a new dictionary 61 | * Implement a function `remove_salt(dict)` which removes 'salt' from the list of 62 | igredients and returns a new dictionary 63 | * Define a list called `FIBONACCI_NUMBERS` which contains the first 12 Fibonacci numbers: 64 | 65 | 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 66 | 67 | * Implement a function `add_fibonacci(lst)` which extends the list of numbers with 68 | the next [Fibonacci number](https://en.wikipedia.org/wiki/Fibonacci_number) 69 | * Implement a function `fib_exists(lst, n)` which returns boolean. The function checks 70 | if the number `n` exists in the Fibonacci sequence `lst` 71 | * Implement a function `which_fib(lst, n)` which returns integer. This is the index 72 | of the number `n` inside the sequence `lst` counting from 1. 73 | 74 | **TIP:** Use the `test.py` file to validate your solution is correct. 75 | -------------------------------------------------------------------------------- /module02/test.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import unittest 3 | from solution import * 4 | 5 | class TestSolution(unittest.TestCase): 6 | def test_num_add(self): 7 | int_result = num_add(1, 2) 8 | # validate type of result b/c Python will consider 3 and 3.0 equal! 9 | self.assertTrue(isinstance(int_result, int)) 10 | self.assertEqual(int_result, 3) 11 | self.assertEqual(num_add(2, 1), 3) 12 | 13 | float_result = num_add(1.5, 2.5) 14 | self.assertTrue(isinstance(float_result, float)) 15 | self.assertEqual(float_result, 4.0) 16 | 17 | def test_num_sub(self): 18 | self.assertEqual(num_sub(1, 2), -1) 19 | self.assertEqual(num_sub(2, 1), 1) 20 | self.assertEqual(num_sub(1.5, 2.5), -1.0) 21 | 22 | def test_num_mul(self): 23 | self.assertEqual(num_mul(3, 2), 6) 24 | self.assertEqual(num_mul(2, 3), 6) 25 | self.assertEqual(num_mul(2.5, 4), 10.0) 26 | 27 | def test_num_div(self): 28 | self.assertEqual(num_div(3, 2), 1.5) 29 | self.assertEqual(num_div(2, 3), 0.6666666666666666) 30 | self.assertEqual(num_div(3.0, 2), 1.5) 31 | 32 | def test_num_floor(self): 33 | self.assertEqual(num_floor(10, 4), 2) 34 | self.assertEqual(num_floor(4, 10), 0) 35 | 36 | def test_num_rem(self): 37 | self.assertEqual(num_rem(10, 4), 2) 38 | self.assertEqual(num_rem(10, 5), 0) 39 | self.assertEqual(num_rem(4, 10), 4) 40 | 41 | def test_boolean_constants(self): 42 | self.assertTrue(IS_TRUE) 43 | self.assertTrue(isinstance(IS_TRUE, bool)) 44 | self.assertFalse(IS_FALSE) 45 | self.assertTrue(isinstance(IS_FALSE, bool)) 46 | 47 | def test_pancake_ingredients(self): 48 | for key in PANCAKE_INGREDIENTS.keys(): 49 | self.assertTrue(key in ['flour', 'eggs', 'milk', 'butter', 'salt']) 50 | self.assertEqual(PANCAKE_INGREDIENTS['flour'], 2) 51 | self.assertEqual(PANCAKE_INGREDIENTS['eggs'], 4) 52 | self.assertEqual(PANCAKE_INGREDIENTS['milk'], 200) 53 | self.assertFalse(PANCAKE_INGREDIENTS['butter']) 54 | self.assertEqual(PANCAKE_INGREDIENTS['salt'], 0.001) 55 | 56 | def test_ingredient_exists(self): 57 | self.assertIs(ingredient_exists('flour', PANCAKE_INGREDIENTS), True) 58 | self.assertIs(ingredient_exists('FLOUR', PANCAKE_INGREDIENTS), False) 59 | self.assertTrue(ingredient_exists('salt', PANCAKE_INGREDIENTS)) 60 | self.assertFalse(ingredient_exists('sugar', PANCAKE_INGREDIENTS)) 61 | 62 | # test that the global PANCAKE_INGREDIENTS variable is not used 63 | # but rather the parameter of this function is used inside its body! 64 | coffee_recipe = { 65 | 'sugar': 1, 66 | 'water': 200, 67 | 'coffee': 1, 68 | 'heat': True 69 | } 70 | self.assertTrue(ingredient_exists('sugar', coffee_recipe)) 71 | 72 | def test_fatten_pancakes(self): 73 | TEST_INGREDIENTS = PANCAKE_INGREDIENTS.copy() 74 | TEST_INGREDIENTS['canary'] = 404 75 | 76 | fat_ingredients = fatten_pancakes(TEST_INGREDIENTS) 77 | 78 | # test that result is a new dictionary 79 | self.assertNotEqual(id(fat_ingredients), id(TEST_INGREDIENTS)) 80 | 81 | self.assertEqual(fat_ingredients['flour'], 2) 82 | self.assertEqual(fat_ingredients['eggs'], 6) 83 | self.assertEqual(fat_ingredients['milk'], 200) 84 | self.assertTrue(fat_ingredients['butter']) 85 | self.assertEqual(fat_ingredients['salt'], 0.001) 86 | 87 | # test that the input parameter was used 88 | self.assertEqual(fat_ingredients['canary'], 404) 89 | 90 | # test that PANCAKE_INGREDIENTS didn't change 91 | self.test_pancake_ingredients() 92 | 93 | def test_add_sugar(self): 94 | TEST_INGREDIENTS = PANCAKE_INGREDIENTS.copy() 95 | TEST_INGREDIENTS['canary'] = 404 96 | 97 | with_sugar = add_sugar(TEST_INGREDIENTS) 98 | 99 | # test that result is a new dictionary 100 | self.assertNotEqual(id(with_sugar), id(TEST_INGREDIENTS)) 101 | 102 | self.assertEqual(with_sugar['flour'], 2) 103 | self.assertEqual(with_sugar['eggs'], 4) 104 | self.assertEqual(with_sugar['milk'], 200) 105 | self.assertFalse(with_sugar['butter']) 106 | self.assertEqual(with_sugar['salt'], 0.001) 107 | self.assertTrue(with_sugar['sugar']) 108 | 109 | # test that the input parameter was used 110 | self.assertEqual(with_sugar['canary'], 404) 111 | 112 | # test that PANCAKE_INGREDIENTS didn't change 113 | self.test_pancake_ingredients() 114 | 115 | def test_remove_salt(self): 116 | TEST_INGREDIENTS = PANCAKE_INGREDIENTS.copy() 117 | TEST_INGREDIENTS['canary'] = 404 118 | 119 | no_salt = remove_salt(TEST_INGREDIENTS) 120 | 121 | # test that result is a new dictionary 122 | self.assertNotEqual(id(no_salt), id(TEST_INGREDIENTS)) 123 | 124 | self.assertFalse('salt' in no_salt.keys()) 125 | 126 | self.assertEqual(no_salt['flour'], 2) 127 | self.assertEqual(no_salt['eggs'], 4) 128 | self.assertEqual(no_salt['milk'], 200) 129 | self.assertFalse(no_salt['butter']) 130 | 131 | # test that the input parameter was used 132 | self.assertEqual(no_salt['canary'], 404) 133 | 134 | # test that PANCAKE_INGREDIENTS didn't change 135 | self.test_pancake_ingredients() 136 | 137 | def test_fibonacci_numbers(self): 138 | self.assertEqual(FIBONACCI_NUMBERS, [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]) 139 | 140 | def test_add_fibonacci(self): 141 | NUMBERS = copy.deepcopy(FIBONACCI_NUMBERS) 142 | self.assertEqual(NUMBERS, [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]) 143 | self.assertEqual(add_fibonacci(NUMBERS)[-1], 233) 144 | self.assertEqual(add_fibonacci(NUMBERS)[-1], 377) 145 | self.assertEqual(add_fibonacci(NUMBERS)[-1], 610) 146 | 147 | def test_fib_exists(self): 148 | # validate input parameter is in use 149 | self.assertIs(fib_exists([1, 1], 2), False) 150 | 151 | self.assertIs(fib_exists(FIBONACCI_NUMBERS, 0), False) 152 | self.assertIs(fib_exists(FIBONACCI_NUMBERS, 1), True) 153 | self.assertTrue(fib_exists(FIBONACCI_NUMBERS, 144)) 154 | 155 | def test_which_fib(self): 156 | # validate input parameter is in use 157 | with self.assertRaises(ValueError): 158 | which_fib([1, 1], 2) 159 | 160 | self.assertEqual(which_fib(FIBONACCI_NUMBERS, 1), 1) 161 | self.assertEqual(which_fib(FIBONACCI_NUMBERS, 55), 10) 162 | 163 | with self.assertRaises(ValueError): 164 | which_fib(FIBONACCI_NUMBERS, 99999) 165 | 166 | 167 | if __name__ == '__main__': 168 | unittest.main() 169 | -------------------------------------------------------------------------------- /module03/README.md: -------------------------------------------------------------------------------- 1 | # Module 03: If statements and loops 2 | 3 | ## Preparation 4 | 5 | * Read sections 8.1, 8.2 and 8.3 from chapter 8 6 | [Compound statements](https://docs.python.org/3/reference/compound_stmts.html) 7 | from Python's documentation to learn the syntax and semantics of `if`, `while` 8 | and `for` statements. 9 | 10 | ## Control flow 11 | 12 | In Python `if` and `while` statements are used to control program flow by 13 | examining boolean conditions. Comparison operators are described in 14 | [6.10 Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons), 15 | boolean operations are described in 16 | [6.11 Boolean operations](https://docs.python.org/3/reference/expressions.html#boolean-operations). 17 | 18 | The `for` statement is used to iterate over sequences. The basic sequence types are 19 | `list` and `str` as well as `tuple`. 20 | 21 | If statements and for loops are the backbone of most Python programs and 22 | are quite handy when writing tests. All tasks below require the usage of both. 23 | 24 | 25 | 26 | ## Tasks & homework 27 | 28 | * Implement all solutions into a file named `solution.py` 29 | 30 | ### Sum of all digits of a number 31 | 32 | * Given an integer, implement a function, called `sum_of_digits(n)` that calculates the sum of n's digits. 33 | * If a negative number is given, our function should work as if it was positive. 34 | * Keep in mind that in Python, there is a special operator for integer division! 35 | 36 | >>> 5 / 2 37 | 2.5 38 | >>> 5 // 2 39 | 2 40 | >>> 5 % 2 41 | 1 42 | 43 | * Function signature 44 | 45 | def sum_of_digits(n): 46 | pass 47 | 48 | ### Turn a number into a list of digits 49 | 50 | * Implement a function, called `to_digits(n)`, which takes an integer `n` and returns a list, containing the digits of `n`. 51 | * Signature 52 | 53 | def to_digits(n): 54 | pass 55 | 56 | 57 | ### Turn a list of digits into a number 58 | 59 | * Implement a function, called `to_number(digits)`, which takes a list of integers - digits and returns the number, containing those digits. 60 | * Signature 61 | 62 | def to_number(digits): 63 | pass 64 | 65 | 66 | ### Vowels in a string 67 | 68 | * Implement a function, called `count_vowels(str)`, which returns the count of all vowels in the string `str`. 69 | **Count uppercase vowels as well!** The English vowels are `aeiouy`. 70 | * Signature 71 | 72 | def count_vowels(str): 73 | pass 74 | 75 | 76 | ### Consonants in a string 77 | 78 | * Implement a function, called `count_consonants(str)`, which returns the count of all consonants in the string `str`. 79 | **Count uppercase consonants as well!** The English consonants are `bcdfghjklmnpqrstvwxz`. 80 | * Signature 81 | 82 | def count_consonants(str): 83 | pass 84 | 85 | ### Prime Number 86 | 87 | * Check if a given number is prime in `prime_number(number)` and return boolean result. 88 | * For the purposes of this task consider 1 to be a prime number as well. 89 | * Hint: 90 | 91 | >>> 5 % 2 92 | 1 93 | 94 | * Signature 95 | 96 | def prime_number(n): 97 | pass 98 | 99 | ### Factorial Digits 100 | 101 | * Implement a function `fact_digits(n)`, that takes an integer and returns the sum of the factorials of each digit of `n`. 102 | * For example, if n = 145, we want 1! + 4! + 5! 103 | * Signature 104 | 105 | def fact_digits(n): 106 | pass 107 | 108 | * Hint - use the functions that you have defined previously. What other functions 109 | do you need ? 110 | 111 | ### First nth members of Fibonacci 112 | 113 | * Implement a function, called `fibonacci(n)` that returns a list with the first `n` members of the Fibonacci sequence. 114 | * Signature 115 | 116 | def fibonacci(n): 117 | pass 118 | 119 | ### Fibonacci number 120 | 121 | * Implement a function, called `fib_number(n)`, which takes an integer `n` and returns a number, 122 | which is formed by concatenating the first `n` Fibonacci numbers. 123 | For example, if `n = 3`, the result is `112`. 124 | * Signature 125 | 126 | def fib_number(n): 127 | pass 128 | 129 | * Hint - use the functions that you have defined previously. What other functions 130 | do you need? 131 | 132 | ### Palindrome 133 | 134 | * Implement a function, called `palindrome(obj)`, 135 | which takes a number or a string and checks if it is a representation is a palindrome. 136 | For example, the integer `121` and the string `"kapak"` are palindromes. The function should work with both. 137 | * Hint - check Python's [str()](https://docs.python.org/3/library/stdtypes.html#str) function 138 | * Signature 139 | 140 | def palindrome(n): 141 | pass 142 | 143 | ### Char Histogram 144 | 145 | * Implement a funcion, called `char_histogram(string)`, which takes a string and returns a dictionary, 146 | where each key is a character from `string` and its value is the number of occurrences of that char in `string`. 147 | * Signature 148 | 149 | def char_histogram(string): 150 | pass 151 | 152 | 153 | **TIP:** Use `test.py` to validate your solution is correct. 154 | -------------------------------------------------------------------------------- /module03/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from solution import * 3 | 4 | class SolutionTests(unittest.TestCase): 5 | def test_char_histogram(self): 6 | self.assertEqual(char_histogram("Python!"), { 'P': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1, 'n': 1, '!': 1 }) 7 | self.assertEqual(char_histogram("AAAAaaa!!!"), { 'A': 4, 'a': 3, '!': 3 }) 8 | 9 | def test_count_consonants(self): 10 | self.assertEqual(count_consonants("Python"), 4) 11 | # It's a volcano name! 12 | self.assertEqual(count_consonants("Theistareykjarbunga"), 11) 13 | self.assertEqual(count_consonants("grrrrgh!"), 7) 14 | self.assertEqual(count_consonants("Github is the second best thing that happend to programmers, after the keyboard!"), 44) 15 | self.assertEqual(count_consonants("A nice day to code!"), 6) 16 | 17 | def test_count_vowels(self): 18 | self.assertEqual(count_vowels("Python"), 2) 19 | # It's a volcano name! 20 | self.assertEqual(count_vowels("Theistareykjarbunga"), 8) 21 | self.assertEqual(count_vowels("grrrrgh!"), 0) 22 | self.assertEqual(count_vowels("Github is the second best thing that happend to programmers, after the keyboard!"), 22) 23 | self.assertEqual(count_vowels("A nice day to code!"), 8) 24 | # empty string returns 0 25 | self.assertEqual(0, count_vowels("")) 26 | 27 | def test_count_vowels_with_none(self): 28 | with self.assertRaises(AttributeError): 29 | count_vowels(None) 30 | 31 | def test_count_negative_value(self): 32 | with self.assertRaises(Exception): 33 | count_vowels(-765) 34 | 35 | def test_fact_digits(self): 36 | self.assertEqual(fact_digits(111), 3) 37 | self.assertEqual(fact_digits(145), 145) 38 | self.assertEqual(fact_digits(999), 1088640) 39 | 40 | def test_factoriel_increases_in_range_1_10(self): 41 | previous = 0 42 | 43 | for x in range(1, 10): 44 | result = fact_digits(x) 45 | # print x, result, previous 46 | # self.assertTrue(result > previous) 47 | assert result > previous 48 | previous = result 49 | 50 | def test_fib_number(self): 51 | self.assertEqual(fib_number(3), 112) 52 | self.assertEqual(fib_number(10), 11235813213455) 53 | 54 | def test_fibonacci(self): 55 | self.assertEqual(fibonacci(1), [1]) 56 | self.assertEqual(fibonacci(2), [1, 1]) 57 | self.assertEqual(fibonacci(3), [1, 1, 2]) 58 | self.assertEqual(fibonacci(10), [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]) 59 | 60 | def test_palindrome(self): 61 | self.assertTrue(palindrome(121)) 62 | self.assertTrue(palindrome("kapak")) 63 | self.assertEqual(palindrome("abba"), True) 64 | self.assertEqual(palindrome("baba"), False) 65 | 66 | def test_prime_number(self): 67 | self.assertEqual(True, prime_number(1)) 68 | self.assertEqual(True, prime_number(2)) 69 | 70 | self.assertEqual(False, prime_number(9)) 71 | self.assertTrue(prime_number(7)) 72 | self.assertEqual(False, prime_number(8)) 73 | 74 | def test_sum_all_digits_of_a_number(self): 75 | self.assertEqual(sum_of_digits(1325132435356), 43) 76 | self.assertEqual(sum_of_digits(123), 6) 77 | self.assertEqual(sum_of_digits(6), 6) 78 | self.assertEqual(sum_of_digits(-10), 1) 79 | 80 | def test_sum_of_digits_with_string_parameter(self): 81 | self.assertRaises(TypeError, sum_of_digits, "Pesho") 82 | # which is the same as 83 | with self.assertRaises(TypeError): 84 | sum_of_digits("Pesho") 85 | 86 | def test_turn_a_number_into_list_of_digits(self): 87 | self.assertEqual(to_digits(123), [1, 2, 3]) 88 | self.assertEqual(to_digits(99999), [9, 9, 9, 9, 9]) 89 | self.assertEqual(to_digits(123023), [1, 2, 3, 0, 2, 3]) 90 | 91 | def test_turn_a_list_of_digits_into_a_number(self): 92 | self.assertEqual(to_number([1, 2, 3]), 123) 93 | self.assertEqual(to_number([3, 2, 1]), 321) 94 | self.assertEqual(to_number([9, 9, 9, 9, 9]), 99999) 95 | 96 | if __name__ == '__main__': 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /module04/01-Cash-Desk/README.md: -------------------------------------------------------------------------------- 1 | # The Cash Desk Problem 2 | 3 | We are going to train our OOP skill by implementing a few classes, which will represent a cash desk. 4 | 5 | The cash desk will do the following things: 6 | 7 | * Take money as single bills 8 | * Take money as batches (пачки!) 9 | * Keep a total count 10 | * Tell us some information about the bills it has 11 | 12 | ## The Bill class 13 | 14 | Create a class, called `Bill` which takes one parameter to its constructor - the `amount` of the bill - an integer. 15 | 16 | This class will only have **dunders** so you won't be afraid of them anymore! 17 | 18 | The class should implement: 19 | 20 | * `__str__` and `__repr__` 21 | * `__int__` 22 | * `__eq__` and `__hash__` 23 | * If amount is negative number, raise an `ValueError` error. 24 | * If type of amount isn't `int`, raise an `TypeError` error. 25 | * **HINT:** raising exceptions is done with `raise ExceptionType("message")` 26 | * See this SO thread about the difference between `__str__` and `__repr__` 27 | http://stackoverflow.com/questions/1436703/difference-between-str-and-repr-in-python 28 | 29 | Here is an example usage: 30 | 31 | ```python 32 | from solution import Bill 33 | 34 | a = Bill(10) 35 | b = Bill(5) 36 | c = Bill(10) 37 | 38 | int(a) # 10 39 | str(a) # "A 10$ bill" 40 | print(a) # A 10$ bill 41 | 42 | a == b # False 43 | a == c # True 44 | 45 | money_holder = {} 46 | 47 | money_holder[a] = 1 # We have one 10$ bill 48 | 49 | if c in money_holder: 50 | money_holder[c] += 1 51 | 52 | print(money_holder) # { "A 10$ bill": 2 } 53 | ``` 54 | 55 | 56 | ## The BatchBill class 57 | 58 | We are going to implement a class, which represents more than one bill. A `BatchBill`! 59 | 60 | The class takes a list of `Bills` as the single constructor argument. 61 | 62 | The class should have the following methods: 63 | 64 | * `__len__(self)` - returns the number of `Bills` in the batch 65 | * `total(self)` - returns the total amount of all `Bills` in the batch 66 | 67 | We should be able to iterate the `BatchBill` class with a for-loop. 68 | 69 | Here is an example: 70 | 71 | ```python 72 | from solution import Bill, BillBatch 73 | 74 | values = [10, 20, 50, 100] 75 | bills = [Bill(value) for value in values] 76 | 77 | batch = BillBatch(bills) 78 | 79 | for bill in batch: 80 | print(bill) 81 | 82 | # A 10$ bill 83 | # A 20$ bill 84 | # A 50$ bill 85 | # A 100$ bill 86 | ``` 87 | 88 | In order to do that, you need to implement the following method: 89 | 90 | ```python 91 | def __getitem__(self, index): 92 | pass 93 | ``` 94 | 95 | ## The CashDesk classs 96 | 97 | Finally, implement a `CashDesk` class, which has the following methods: 98 | 99 | * `take_money(money)`, where `money` can be either `Bill` or `BatchBill` class 100 | * `total()` - returns the total amount of money currenly in the desk 101 | * `inspect()` - returns a table representation of the money - for each bill, how many copies of it we have. 102 | 103 | For example: 104 | 105 | ```python 106 | from solution import Bill, BillBatch, CashDesk 107 | 108 | values = [10, 20, 50, 100, 100, 100] 109 | bills = [Bill(value) for value in values] 110 | 111 | batch = BillBatch(bills) 112 | 113 | desk = CashDesk() 114 | 115 | desk.take_money(batch) 116 | desk.take_money(Bill(10)) 117 | 118 | print(desk.total()) # 390 119 | desk.inspect() 120 | 121 | # We have a total of 390$ in the desk 122 | # We have the following count of bills, sorted in ascending order: 123 | # 10$ bills - 2 124 | # 20$ bills - 1 125 | # 50$ bills - 1 126 | # 100$ bills - 3 127 | 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /module04/01-Cash-Desk/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from solution import Bill, BatchBill, CashDesk 3 | 4 | 5 | class TestBill(unittest.TestCase): 6 | def setUp(self): 7 | self.bill = Bill(5) 8 | 9 | def test_bill_str(self): 10 | self.assertEqual(str(self.bill), "A 5$ bill") 11 | 12 | def test_bill_int(self): 13 | self.assertEqual(int(self.bill), 5) 14 | 15 | def test_bill_eq(self): 16 | bill2 = Bill(10) 17 | bill3 = Bill(5) 18 | self.assertNotEqual(self.bill, bill2) 19 | self.assertEqual(False, self.bill == bill2) 20 | self.assertEqual(self.bill, bill3) 21 | 22 | def test_type_of_amount(self): 23 | with self.assertRaises(TypeError): 24 | Bill("10") 25 | 26 | def test_value_of_amount(self): 27 | with self.assertRaises(ValueError): 28 | Bill(-5) 29 | 30 | 31 | class TestBatchBill(unittest.TestCase): 32 | def setUp(self): 33 | self.bill5 = Bill(5) 34 | self.bill10 = Bill(10) 35 | self.batch = BatchBill([self.bill5, self.bill10]) 36 | 37 | def test_batchbill_init(self): 38 | self.assertIn(self.bill5, self.batch) 39 | self.assertIn(self.bill10, self.batch) 40 | 41 | def test_batchbill_total(self): 42 | self.assertEqual(self.batch.total(), 15) 43 | 44 | 45 | class TestCashDesk(unittest.TestCase): 46 | def setUp(self): 47 | self.bill = Bill(10) 48 | self.batch = BatchBill([Bill(5), Bill(10), Bill(15)]) 49 | self.desk = CashDesk() 50 | 51 | def test_take_money_from_bill(self): 52 | self.desk.take_money(self.bill) 53 | self.assertEqual(self.desk.total(), 10) 54 | 55 | def test_take_money_from_batch(self): 56 | self.desk.take_money(self.batch) 57 | self.assertEqual(self.desk.total(), 30) 58 | 59 | def test_cashdesk_total(self): 60 | self.desk.take_money(self.bill) 61 | self.desk.take_money(self.batch) 62 | self.assertEqual( 63 | self.desk.total(), 40) 64 | 65 | def test_cashdesk_inspect_value(self): 66 | self.desk.take_money(self.bill) 67 | self.desk.take_money(self.batch) 68 | 69 | expected = """We have a total of 40$ in the desk 70 | We have the following count of bills, sorted in ascending order: 71 | 5$ bills - 1 72 | 10$ bills - 2 73 | 15$ bills - 1""" 74 | 75 | self.assertEqual(self.desk.inspect(), expected) 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /module04/02-Bank-Account/README.md: -------------------------------------------------------------------------------- 1 | # A Bank Account 2 | 3 | `BankAccount` class, which behaves like that: 4 | 5 | ## Basic BankAccount usage 6 | 7 | Our `BankAccount` will have the following methods: 8 | 9 | * Constructor takes a `name` for the account, initial `balance` and a `currency`. 10 | If `balance` is negative number, raise a `ValueError` error. 11 | If `currency` is not passed, raise a `ValueError` error. 12 | * `deposit(amount)` - deposits money of `amount` to your balance. 13 | If `amount` is negative number, raise a `ValueError` error. 14 | * `balance()` - returns the current balance 15 | * `withdraw(amount)` - takes `amount` money from the account. Returns `True` if it was successful. Otherwise, `False` 16 | * `__str__` should return: `"Bank account for {name} with balance of {amount}{currency}"` 17 | * `__int__` should return the balance of the `BankAccount` 18 | * `history()` - returns a list of strings, that represent the history of the bank account. Check examples below for more information. 19 | 20 | 21 | ```python 22 | >>> account = BankAccount("Rado", 0, "$") 23 | >>> print(account) 24 | 'Bank account for Rado with balance of 0$' 25 | >>> account.deposit(1000) 26 | >>> account.balance() 27 | 1000 28 | >>> str(account) 29 | 'Bank account for Rado with balance of 1000$' 30 | >>> int(account) 31 | 1000 32 | >>> account.history() 33 | ['Account was created', 'Deposited 1000$', 'Balance check -> 1000$', '__int__ check -> 1000$'] 34 | >>> account.withdraw(500) 35 | True 36 | >>> account.balance() 37 | 500 38 | >>> account.history() 39 | ['Account was created', 'Deposited 1000$', 'Balance check -> 1000$', '__int__ check -> 1000$', '500$ was withdrawn', 'Balance check -> 500$'] 40 | >>> account.withdraw(1000) 41 | False 42 | >>> account.balance() 43 | 500 44 | >>> account.history() 45 | ['Account was created', 'Deposited 1000$', 'Balance check -> 1000$', '__int__ check -> 1000$', '500$ was withdrawn', 'Balance check -> 500$', 'Withdraw for 1000$ failed.', 'Balance check -> 500$'] 46 | ``` 47 | 48 | ## Extra usage 49 | 50 | Also, we should be able to transfer money from one account to another: 51 | 52 | * `transfer_to(account, amount)` - transfers `amount` to `account` if they both have the same currencies! Returns `True` if successful. 53 | 54 | ```python 55 | >>> rado = BankAccount("Rado", 1000, "BGN") 56 | >>> ivo = BankAccount("Ivo", 0, "BGN") 57 | >>> rado.transfer_to(ivo, 500) 58 | True 59 | >>> rado.balance() 60 | 500 61 | >>> ivo.balance() 62 | 500 63 | >>> rado.history() 64 | ['Account was created', 'Transfer to Ivo for 500BGN', 'Balance check -> 500BGN'] 65 | >>> ivo.history() 66 | ['Account was created', 'Transfer from Rado for 500BGN', 'Balance check -> 500BGN'] 67 | ``` 68 | -------------------------------------------------------------------------------- /module04/02-Bank-Account/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime 3 | from solution import BankAccount 4 | 5 | 6 | class TestBankAccount(unittest.TestCase): 7 | def setUp(self): 8 | self.account = BankAccount("Rado", 0, "$") 9 | 10 | def test_can_create_bank_account(self): 11 | '''' 12 | Sanity test that nothing went wrong in the init method. 13 | Not really a very practical one but students are full of suprprizes 14 | ''' 15 | self.assertTrue(isinstance(self.account, BankAccount)) 16 | # which is the same as 17 | self.assertIsInstance(self.account, BankAccount) 18 | 19 | def test_initial_zero_balance(self): 20 | self.assertEqual(self.account.balance(), 0) 21 | 22 | def test_good_example_of_using_with_block(self): 23 | account = BankAccount("Test", 100, "BGN") 24 | # .... 100 more lines of code 25 | #raise ValueError('Can you find me!') 26 | # and few more 27 | with self.assertRaises(ValueError): 28 | account.deposit(-10) 29 | 30 | def test_bad_example_of_using_with_block(self): 31 | with self.assertRaises(ValueError): 32 | account = BankAccount("Test", 100, "") 33 | # DON'T stuff everything inside a with block 34 | # b/c you may catch exceptions which are not coming 35 | # from the method under test and you will never know 36 | # about them 37 | raise ValueError('Can you find me!') 38 | # .... 100 more lines of code 39 | account.deposit(-10) 40 | 41 | def test_negative_initial_amount(self): 42 | with self.assertRaises(ValueError): 43 | acc = BankAccount("Test", -100, "$") 44 | BankAccount("Test", 100, "") 45 | 46 | # this is not possible b/c acc is available only 47 | # inside the with block. 48 | # print acc 49 | 50 | with self.assertRaises(ValueError): 51 | BankAccount("Test", 100, "") 52 | 53 | def test_init_with_empty_currency(self): 54 | with self.assertRaises(ValueError): 55 | BankAccount("Test", 100, "") 56 | 57 | def test_deposit_in_empty_account(self): 58 | self.account.deposit(500) 59 | self.assertEqual(self.account.balance(), 500) 60 | 61 | def test_deposit_in_not_empty_account(self): 62 | account = BankAccount("Ivo", 1000, "$") 63 | account.deposit(500) 64 | self.assertEqual(account.balance(), 1500) 65 | 66 | def test_deposit_negative_amount(self): 67 | with self.assertRaises(ValueError): 68 | self.account.deposit(-100) 69 | 70 | def test_withdraw_from_not_empty_account(self): 71 | self.account.deposit(100) 72 | result = self.account.withdraw(50) 73 | 74 | self.assertTrue(result) 75 | self.assertEqual(self.account.balance(), 50) 76 | 77 | def test_withdraw_from_empty_account(self): 78 | result = self.account.withdraw(50) 79 | 80 | self.assertIsNotNone(result) 81 | self.assertFalse(result) 82 | 83 | def test_history(self): 84 | account = BankAccount("Test", 0, "$") 85 | account.deposit(20) 86 | account.balance() 87 | int(account) 88 | expected = ["Account was created", "Deposited 20$", 89 | "Balance check -> 20$", "__int__ check -> 20$"] 90 | 91 | self.assertEqual(account.history(), expected) 92 | 93 | 94 | class TestBankAccount_transfer_to(unittest.TestCase): 95 | ''' 96 | It is also possible to create test classes that test a single 97 | method. In this case we want a separate class for testing the 98 | BankAccount.transfer_to() method because there are several 99 | scenarios and we want to better organize them. 100 | ''' 101 | def setUp(self): 102 | self.account = BankAccount('For testing', 100, 'BGN') 103 | 104 | def test_transfer_to_without_parameters(self): 105 | # raises when other account is missing 106 | with self.assertRaises(TypeError): 107 | self.account.transfer_to(1) 108 | 109 | # can be written as 110 | self.assertRaises(TypeError, self.account.transfer_to, 1) 111 | 112 | # raises when how_miuch is missing 113 | other_account = BankAccount('with zero balance', 0, '$') 114 | with self.assertRaises(TypeError): 115 | self.account.transfer_to(other_account) 116 | # this is not necessary ???? 117 | self.assertEqual(other_account.balance(), 0) 118 | self.assertEqual(self.account.balance(), 100) 119 | 120 | def test_transfer_between_different_currencies_not_possible(self): 121 | leva_account = BankAccount('For testing', 100, 'BGN') 122 | dollar_account = BankAccount('In dollars', 10, '$') 123 | 124 | with self.assertRaises(TypeError): 125 | leva_account.transfer_to(dollar_account, 50) 126 | 127 | self.assertEqual(leva_account.balance(), 100) 128 | self.assertEqual(dollar_account.balance(), 10) 129 | 130 | def test_transfer_negative_amount(self): 131 | account_1 = BankAccount('For testing', 100, '$') 132 | account_2 = BankAccount('In dollars', 10, '$') 133 | 134 | with self.assertRaises(ValueError): 135 | account_1.transfer_to(account_2, -50) 136 | 137 | self.assertEqual(account_1.balance(), 100) 138 | self.assertEqual(account_2.balance(), 10) 139 | 140 | 141 | def test_transfer_positive_mount_should_work(self): 142 | account_1 = BankAccount('For testing', 100, '$') 143 | account_2 = BankAccount('In dollars', 10, '$') 144 | 145 | account_1.transfer_to(account_2, 50) 146 | 147 | self.assertEqual(account_1.balance(), 50) 148 | self.assertEqual(account_2.balance(), 60) 149 | 150 | def test_transfer_more_than_vailable_balance_should_fail(self): 151 | account_1 = BankAccount('For testing', 100, '$') 152 | account_2 = BankAccount('In dollars', 10, '$') 153 | 154 | with self.assertRaises(Exception): 155 | account_1.transfer_to(account_2, 150) 156 | 157 | self.assertEqual(account_1.balance(), 100) 158 | self.assertEqual(account_2.balance(), 10) 159 | 160 | 161 | if __name__ == '__main__': 162 | unittest.main() 163 | -------------------------------------------------------------------------------- /module04/03-Panda-Social-Network/README.md: -------------------------------------------------------------------------------- 1 | We are going to make a social network for Pandas 2 | 3 | This is the next big thing. We promise! 4 | 5 | # Panda 6 | 7 | For our social network, we are going to need a `Panda` class which behaves like that: 8 | 9 | ```python 10 | ivo = Panda("Ivo", "ivo@pandamail.com", "male") 11 | 12 | ivo.name() == "Ivo" # True 13 | ivo.email() == "ivo@pandamail.com" # True 14 | ivo.gender() == "male" # True 15 | ivo.is_male() == True # True 16 | ivo.is_female() == False # True 17 | ``` 18 | 19 | The `Panda` class also should be possible to: 20 | 21 | * Be turned into a string 22 | * Be hashed and used as a key in a dictionary (`__eq__` and `__hash__`) 23 | * Make sure that the email is a valid email! 24 | 25 | Two `Panda` instances are equal if they have matching `name`, `email` and `gender` attributes. 26 | 27 | # SocialNetwork 28 | 29 | Now it is time for our social network! 30 | 31 | Implement a class, called `PandaSocialNetwork`, which has the following public methods: 32 | 33 | * `add_panda(panda)` - this method adds a panda to the social network. The panda has 0 friends for now. 34 | If the panda is already in the network, raise a `PandaAlreadyThere` error. 35 | * `has_panda(panda)` - returns `True` or `False` if the panda is in the network or not. 36 | * `make_friends(panda1, panda2)` - makes the two pandas friends. Raise `PandasAlreadyFriends` if they are already friends. 37 | The friendship is two-ways - `panda1` is a friend with `panda2` and `panda2` is a friend with `panda1`. 38 | If `panda1` or `panda2` are not members of the network, add them! 39 | * `are_friends(panda1, panda2)` - returns `True` if the pandas are friends. Otherwise, `False` 40 | * `friends_of(panda)` - returns a list of `Panda` with the friends of the given panda. 41 | Returns `False` if the panda is not a member of the network. 42 | 43 | # Extra homework 44 | 45 | * `connection_level(panda1, panda2)` - returns the connection level between `panda1` and `panda2`. 46 | If they are friends, the level is 1. Otherwise, count the number of friends you need to go 47 | through from `panda1` in order to get to `panda2`. 48 | If they are not connected at all, return -1! 49 | Return `False` if one of the pandas are not member of the network. 50 | * `are_connected(panda1, panda2)` - return `True` if the pandas are connected somehow, between friends, or `False` otherwise. 51 | * `how_many_gender_in_network(level, panda, gender)` - returns the number of pandas with `gender` (male of female) that 52 | are in the network of `panda`, while counting `level` levels deep. 53 | If level == 2, we will have to look in all friends of `panda` and all of their friends too... 54 | 55 | **NOTE 1:** the above 3 methods are recursive! For more info on recursions see: 56 | http://www.python-course.eu/recursive_functions.php 57 | 58 | **NOTE 2:** each recursive function can be rewriten in iterative way. Which one is easier and 59 | more readable/simple depends on the particular problem domain. 60 | 61 | **NOTE 3:** during testing we don't need to use recursion. If we do then maybe we're 62 | doing something wrong! 63 | 64 | An example 65 | 66 | ```python 67 | network = PandaSocialNetwork() 68 | ivo = Panda("Ivo", "ivo@pandamail.com", "male") 69 | rado = Panda("Rado", "rado@pandamail.com", "male") 70 | tony = Panda("Tony", "tony@pandamail.com", "female") 71 | 72 | for panda in [ivo, rado, tony]: 73 | network.add(panda) 74 | 75 | network.make_friends(ivo, rado) 76 | network.make_friends(rado, tony) 77 | 78 | network.connection_level(ivo, rado) == 1 # True 79 | network.connection_level(ivo, tony) == 2 # True 80 | 81 | network.how_many_gender_in_network(1, rado, "female") == 1 # True 82 | ``` 83 | -------------------------------------------------------------------------------- /module04/03-Panda-Social-Network/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from solution import Panda, PandaSocialNetwork 4 | 5 | 6 | class TestPanda(unittest.TestCase): 7 | def setUp(self): 8 | self.panda = Panda("Ivo", "ivo@pandamail.com", "male") 9 | 10 | def test_name_and_email_and_gender(self): 11 | self.assertEqual(self.panda.name(), "Ivo") 12 | self.assertEqual(self.panda.email(), "ivo@pandamail.com") 13 | self.assertEqual(self.panda.gender(), "male") 14 | 15 | def test_is_male_and_is_female(self): 16 | self.assertFalse(self.panda.is_female()) 17 | self.assertTrue(self.panda.is_male()) 18 | 19 | def test_pandas_are_equal(self): 20 | ivo = Panda("Ivo", "ivo@pandamail.com", "male") 21 | rado = Panda("Ivo", "ivo@pandamail.com", "female") 22 | 23 | self.assertFalse(self.panda == rado) 24 | self.assertTrue(self.panda == ivo) 25 | 26 | 27 | class TestSocialNetwork(unittest.TestCase): 28 | def setUp(self): 29 | self.network = PandaSocialNetwork() 30 | self.ivo = Panda("Ivo", "ivo@pandamail.com", "male") 31 | 32 | def test_name_email_gender_hash(self): 33 | self.assertEqual(self.ivo.name(), "Ivo") 34 | self.assertEqual(self.ivo.email(), "ivo@pandamail.com") 35 | self.assertEqual(self.ivo.gender(), "male") 36 | 37 | def test_add_and_has_panda_in_network(self): 38 | self.network.add_panda(self.ivo) 39 | 40 | self.assertTrue(self.network.has_panda(self.ivo)) 41 | 42 | def test_has_panda_when_panda_not_in_network(self): 43 | self.assertFalse(self.network.has_panda(self.ivo)) 44 | 45 | def test_make_and_are_friends(self): 46 | rado = Panda("Rado", "rado@pandamail.com", "male") 47 | self.network.make_friends(self.ivo, rado) 48 | 49 | self.assertTrue(self.network.are_friends(self.ivo, rado)) 50 | 51 | def test_friends_of_panda(self): 52 | rado = Panda("Rado", "rado@pandamail.com", "male") 53 | pavli = Panda("Pavli", "pavlin@pandamail.com", "male") 54 | maria = Panda("maria", "maria@pandamail.com", "female") 55 | 56 | self.network.make_friends(self.ivo, rado) 57 | self.network.make_friends(self.ivo, pavli) 58 | self.network.make_friends(self.ivo, maria) 59 | 60 | expected = [rado, pavli, maria] 61 | self.assertEqual(self.network.friends_of(self.ivo), expected) 62 | 63 | def test_friends_of_panda_when_panda_not_in_network(self): 64 | self.assertFalse(self.network.friends_of(self.ivo)) 65 | 66 | @unittest.skip('May not be implemented') 67 | def test_connection_level_between_two_pandas(self): 68 | rado = Panda("Rado", "rado@pandamail.com", "male") 69 | pavli = Panda("Pavli", "pavlin@pandamail.com", "male") 70 | maria = Panda("maria", "maria@pandamail.com", "female") 71 | gogo = Panda("Gogo", "gogo@pandamail.com", "male") 72 | 73 | self.network.make_friends(self.ivo, rado) 74 | self.network.make_friends(pavli, gogo) 75 | self.network.make_friends(rado, pavli) 76 | self.network.make_friends(pavli, maria) 77 | 78 | self.assertEqual(self.network.connection_level(self.ivo, rado), 1) 79 | self.assertEqual(self.network.connection_level(self.ivo, pavli), 2) 80 | self.assertEqual(self.network.connection_level(self.ivo, maria), 3) 81 | 82 | @unittest.skip('May not be implemented') 83 | def test_are_connected(self): 84 | rado = Panda("Rado", "rado@pandamail.com", "male") 85 | pavli = Panda("Pavli", "pavlin@pandamail.com", "male") 86 | maria = Panda("maria", "maria@pandamail.com", "female") 87 | 88 | self.network.make_friends(self.ivo, rado) 89 | self.network.make_friends(rado, pavli) 90 | 91 | self.assertTrue(self.network.are_connected(self.ivo, pavli)) 92 | self.assertFalse(self.network.are_connected(self.ivo, maria)) 93 | 94 | @unittest.skip('May not be implemented') 95 | def test_connection_level_when_panda_has_no_friends(self): 96 | rado = Panda("Rado", "rado@pandamail.com", "male") 97 | self.network.add_panda(rado) 98 | self.network.add_panda(self.ivo) 99 | 100 | self.assertEqual(self.network.connection_level(self.ivo, rado), -1) 101 | 102 | @unittest.skip('May not be implemented') 103 | def test_connection_level_when_panda_not_in_network(self): 104 | rado = Panda("Rado", "rado@pandamail.com", "male") 105 | self.network.add_panda(rado) 106 | 107 | self.assertFalse(self.network.connection_level(self.ivo, rado)) 108 | 109 | @unittest.skip('May not be implemented') 110 | def test_how_many_genders_in_network(self): 111 | rado = Panda("Rado", "rado@pandamail.com", "male") 112 | pavli = Panda("Pavlin", "pavlin@pandamail.com", "male") 113 | alex = Panda("Alex", "alex@pandamail.com", "male") 114 | maria = Panda("maria", "maria@pandamail.com", "female") 115 | slavyana = Panda("Slavyana", "slavyana@pandamail.com", "female") 116 | 117 | self.network.make_friends(self.ivo, rado) 118 | self.network.make_friends(pavli, slavyana) 119 | self.network.make_friends(rado, pavli) 120 | self.network.make_friends(pavli, maria) 121 | self.network.make_friends(self.ivo, pavli) 122 | self.network.make_friends(maria, alex) 123 | 124 | self.assertEqual(self.network.how_many_gender_in_network( 125 | 1, self.ivo, "male"), 2) 126 | self.assertEqual(self.network.how_many_gender_in_network( 127 | 1, self.ivo, "female"), 0) 128 | self.assertEqual(self.network.how_many_gender_in_network( 129 | 2, self.ivo, "female"), 2) 130 | self.assertEqual(self.network.how_many_gender_in_network( 131 | 25, self.ivo, "male"), 3) 132 | 133 | 134 | if __name__ == '__main__': 135 | unittest.main() 136 | -------------------------------------------------------------------------------- /module04/README.md: -------------------------------------------------------------------------------- 1 | # Module 04: Object Oriented Programming 2 | 3 | ## Preparation 4 | 5 | * Read Chapter 5 from Dive into Python; 6 | * Read [Chapter 9. Classes](https://docs.python.org/3/tutorial/classes.html) 7 | from the Python tutorial; 8 | * Read this file! 9 | 10 | 11 | Object Oriented Programming is a programming paradigm, that's all about representing concepts with 12 | objects, classes, methods and members. 13 | 14 | Popular object oriented languages are C++, Java, Python and Ruby. 15 | 16 | 17 | ## The four principles of OOP 18 | 19 | - Abstraction - wrapping common characteristics, states and functionality in abstract structures 20 | - Encapsulation - hiding implementation details and accessing functionality through public interfaces 21 | - Inheritance - reusing code while creating superstructures, also known as being DRY* 22 | - Polymorphism - the provision of a single interface to entities of different types 23 | 24 | 25 | ## Glossary 26 | 27 | The fancy words we're going to use. 28 | 29 | * Class - an abstraction (ex. Person, Vehicle..) 30 | * Object - an instance of class 31 | * Member/field/attribute - a class variable 32 | * Method - a class function 33 | * DRY - Don't Repeat Yourself 34 | * WET - We Enjoy Typing (opposite of DRY) 35 | 36 | 37 | # Python OOP by example 38 | 39 | ## Creating a class (abstraction) 40 | 41 | ```python 42 | class Panda: 43 | 44 | def __init__(self, name, age, weight): 45 | self.name = name 46 | self.age = age 47 | self.weight = weight 48 | 49 | def _get_buff(self): 50 | if self.weight < 1000: 51 | self.weight += 1 52 | 53 | def eat_bamboo(self): 54 | self._get_buff() 55 | return "Nomm nomm nomm!" 56 | 57 | 58 | dimcho = Panda("Dimcho", 10, 1500) 59 | print(dimcho.age) # 10 60 | print(dimcho.eat_bamboo()) # "Nomm nomm nomm!" 61 | ``` 62 | 63 | The key concepts here are: 64 | 65 | * The initialiser (aka constructor) method for each class is called `__init__` 66 | * Each method takes `self` as the first argument - this is a reference to the current instance of the class. 67 | * **Everything is public!** Private and protected are only a notation - something that we all agree on. 68 | More on private and public later. 69 | 70 | ## How to check if object is an instance of a class? 71 | 72 | We can also check if an object is the instance of a class. 73 | 74 | For example, let's say we have an object of the class Human and we want to check 75 | if it's instance (also subclass) of class Object. 76 | 77 | ```python 78 | class Human: 79 | 80 | def __init__(self, name, age, weight): 81 | self.name = name 82 | 83 | gosho = Human('Gosho', 20, 70) 84 | 85 | print(isinstance(gosho, Human)) # True 86 | print(isinstance(gosho, object)) # True 87 | print(isinstance(gosho, Panda)) # False 88 | ``` 89 | 90 | 91 | ## Magic methods 92 | 93 | In Python, there are special merhods, called **dunders** - short for double underscore. 94 | 95 | They give us flexibility and can be very powerful! They also implement some standard 96 | behavior. 97 | 98 | For example, if you want the `str()` function to work with your class, you should implement the `__str__(self)` method. 99 | 100 | 101 | ```python 102 | class Panda: 103 | 104 | def __init__(self, name, age, weight): 105 | self.name = name 106 | self.age = age 107 | self.weight = weight 108 | 109 | def _get_buff(self): 110 | if self.weight < 1000: 111 | self.weight += 1 112 | 113 | def eat_bamboo(self): 114 | self._get_buff() 115 | return "Nomm nomm nomm!" 116 | 117 | def __str__(self): 118 | return "I am a panda - {}".format(self.name) 119 | 120 | 121 | dimcho = Panda("Dimchou", 10, 1500) 122 | print(str(dimcho)) # "I am a panda - Dimchou" 123 | ``` 124 | 125 | We are going to use the following: 126 | 127 | * If we want to compare two instances of our class - `__eq__(self, other)` 128 | * If we want to turn our instance into a string - `__str__(self)` 129 | * If we want to print a string representation of our instance - `__repr__(self)` 130 | * If we want to turn our instance to a `int()` - `__int__(self)` 131 | * If we want to make our class hashable - `__hash__(self)` 132 | 133 | You can find more in the [Python data model](https://docs.python.org/3.4/reference/datamodel.html) 134 | 135 | ## Static methods and fields 136 | 137 | * Static fields are shared between all classes of that type. (class `Panda` in our case). 138 | * Static methods are neither obligated to have `self` as a first parameter, nor use objects from the same class as the static method's. 139 | But it's good to have Panda classes do only panda stuff! :panda_face: 140 | 141 | 142 | Static fields in action: 143 | 144 | ```python 145 | class Panda: 146 | all_pandas = [] 147 | total_pandas_mass = 0 148 | 149 | def __init__(self, name, weight): 150 | self.name = name 151 | self.weight = weight 152 | Panda.total_pandas_mass += weight 153 | Panda.all_pandas.append(self) 154 | 155 | def __repr_(self): 156 | return self.name 157 | 158 | 159 | dimcho = Panda("Dimcho", 50) 160 | print(Panda.all_pandas()) # ['Dimcho'] 161 | 162 | boko = Panda("Boko", 70) 163 | print(Panda.total_pandas_mass) # 120 164 | ``` 165 | 166 | Static methods in action: 167 | 168 | ```python 169 | class Panda: 170 | all_pandas = [] 171 | pandas_count = 0 172 | 173 | def __init__(self, name): 174 | self.name = name 175 | Panda.pandas_count += 1 176 | Panda.all_pandas.append(name) 177 | 178 | @staticmethod 179 | def print_all_pandas(): 180 | for panda in Panda.all_pandas: 181 | print(panda.name) 182 | 183 | # Possible! But don't do so :( 184 | @staticmethod 185 | def calculate_difference(a, b): 186 | return a - b 187 | 188 | dimcho = Panda("Dimcho") 189 | Panda.print_all_pandas() # Dimcho 190 | print(Panda.calculate_difference(10 - 5)) # 5 191 | # Again, don't make poor pandas do math please! 192 | ``` 193 | 194 | 195 | ## Inheritance 196 | 197 | Classes in Python can inherit from other classes in order to implementspecific behavior. 198 | To access methods and fields from the parent class use `super()`. 199 | 200 | ### Extra glossary 201 | 202 | * Parent class - also known as a base class (ex. Panda) 203 | * Child class - also known as sub class or inherited class (ex. KungFuPanda) 204 | 205 | 206 | **Creating a child class:** 207 | 208 | ```python 209 | class Panda: 210 | 211 | def __init__(self, name, age, weight): 212 | self.name = name 213 | self.age = age 214 | self.weight = weight 215 | 216 | def _get_fatter(self): 217 | if self.weight < 1000: 218 | self.weight += 1 219 | 220 | def eat(self): 221 | self._get_fatter() 222 | print("Nomm nomm nomm! Bamboo.") 223 | 224 | 225 | class KungFuPanda(Panda): 226 | 227 | def __init__(self, name, age, weight, skill): 228 | super(KungFuPanda, self).__init__(name, age, weight) 229 | self.skill = skill 230 | 231 | def fight(self): 232 | self.weight -= 1 233 | print("Bam bam!") 234 | 235 | 236 | po = KungFuPanda("Po", 5, 700, 10) 237 | po.eat() # Nomm nomm nomm! Bamboo. 238 | ``` 239 | 240 | In Python all methods are effectively virtual. This means we can override a parent method by just 241 | defining a method with the same name in a child class. 242 | That means we can make our `KungFuPanda`s eat rice! 243 | 244 | 245 | ```python 246 | class KungFuPanda(Panda): 247 | 248 | def __init__(self, name, age, weight, skill): 249 | super(KungFuPanda, self).__init__(name, age, weight) 250 | self.skill = skill 251 | 252 | def fight(self): 253 | self.weight -= 1 254 | print("Bam bam!") 255 | 256 | def eat(self): 257 | self._get_fatter() 258 | self._get_fatter() 259 | print("Nomm nomm nomm! Rice.") 260 | 261 | po = KungFuPanda("Po", 5, 700, 10) 262 | po.eat() # Nomm nomm nomm! Rice. 263 | ``` 264 | 265 | 266 | ## Protected and private fields 267 | 268 | You don't have an actual protected privacy in Python. This means protected fields can be accessed by everyone, 269 | but developers will know not to do so! (It also makes testing easier) 270 | 271 | * Names that starts with _ are protected 272 | * Names that starts with __ are private 273 | 274 | 275 | ```python 276 | class Panda: 277 | def __init__(self): 278 | self._dna = 'pandish' 279 | self.__power = 42 280 | 281 | jorko = Panda() 282 | print(jorko._dna) # pandish 283 | print(jorko.__power) # AttributeError: 'Panda' object has no attribute '__power' 284 | ``` 285 | 286 | ## Polymorphism 287 | 288 | At last, let's show how we can manage all Pandas. 289 | 290 | ```python 291 | class Panda: 292 | 293 | def __init__(self, name, age, weight): 294 | self.name = name 295 | self.age = age 296 | self.weight = weight 297 | 298 | def eat(self): 299 | return "nomnom nom" 300 | 301 | 302 | class PandaCareTaker(): 303 | 304 | ..... 305 | 306 | def feed_panda(panda): 307 | panda.eat() 308 | print("I fed {}".format(panda.name)) 309 | 310 | 311 | boko = Panda('Boko', 5, 200) 312 | jacky_chan = PandaCareTaker('Jacky', 60, 67) 313 | 314 | jacky_chan.feed_panda(boko) # I fed Boko 315 | ``` 316 | 317 | 318 | ## Tasks & homework 319 | 320 | * Checkout the 3 tasks in this directory; 321 | * Use the `test.py` file in each directory to validate your implementation! 322 | -------------------------------------------------------------------------------- /module05/README.md: -------------------------------------------------------------------------------- 1 | # Module 05: Testing in Python 2 | 3 | ## Preparation 4 | 5 | * Read Chapter [26.4. unittest - Unit testing framework](https://docs.python.org/3/library/unittest.html); 6 | 7 | 8 | In Python tests should inherit the standard `unittest.TestCase` class. Notable 9 | methods are: 10 | 11 | * `setUp(self)` - initialization of test environment; This is called immediately before calling the test method; 12 | * `tearDown(self)` - clean up of test environment; Method called immediately after the test method has been called 13 | and the result recorded. This is called even if the test method raised an exception, so the implementation in 14 | subclasses may need to be particularly careful about checking internal state; 15 | * `setUpClass()` - A class method called before tests in an individual class run. setUpClass is called with 16 | the class as the only argument and must be decorated as a `@classmethod`; 17 | * `tearDownClass()`- A class method called after tests in an individual class have run. tearDownClass is called 18 | with the class as the only argument and must be decorated as a `@classmethod`; 19 | * `runTest(self)` - if all tests go into one method or 20 | * `test_XYZ(self)` - for testing specific functionality 21 | * `assertXYZ(...)` - for asserting various conditions 22 | 23 | The standard documentation contains the 24 | [list of assert methods](https://docs.python.org/3/library/unittest.html#assert-methods)! 25 | 26 | You can write asserts using several different styles: 27 | 28 | assert 4 == 4 29 | self.assertEqual(4, 4) 30 | self.assertTrue(4 == 4) 31 | self.assertTrue(-1) 32 | 33 | assert 4 < 5 34 | self.assertTrue(4 < 5) 35 | 36 | 37 | ## Demonstration of setUp/tearDown and test execution 38 | 39 | Execute `test_example.py` to see the order of execution of all set-up/tear-down and test methods! 40 | 41 | 42 | ## Flaky tests 43 | 44 | These are tests which randomly fail without an obvious reason. The root cause behind them 45 | is either timing issues (async JavaScript in a web context) or mismatch between the actual 46 | environment the test is running into (DB records, files on disk, etc) and the environment 47 | the tester imagined when the test was created! Execute `test_example.py` several times quickly 48 | to trigger a flaky failure. 49 | 50 | ## Object oriented principles for testers 51 | 52 | class MyTestCase(unittest.TestCase): 53 | """ 54 | This is a class definition. A class describes how something works 55 | but it doesn't physically exist! A class is an abstration, it tells 56 | us what the general behavior is via it's methods (functions inside the class) 57 | and attributes (variables inside the object). 58 | 59 | 60 | An object is an instance of a class. An object exists into memory and is 61 | assigned to a variable. We can create objects as we like, execute their 62 | methods (via obj_name.method_name()) and 63 | access their attributes (via obj_name.attr_name). 64 | 65 | 66 | In Python all tests must inherit from unittest.TestCase. This means 67 | that the unittest.TestCase class must be inside the inheritance list 68 | when defining the class as shown above! 69 | """ 70 | 71 | def setUp(self): 72 | """ 73 | All methods of the class receive a first parameter called self. 74 | Python does this automatically but we have to declare it! 75 | 76 | The self variable represents the current instance of this class. 77 | We can use it to access attributes and call other methods of the same 78 | class! 79 | """ 80 | # here we assign a value to the name attribute. 81 | # self.name is accessible outside this method 82 | self.name = 'Gosho' 83 | 84 | # on the other hand ime is a local variable. 85 | # it will not be accessible outside this method 86 | ime = 'Ivan' 87 | 88 | def test_something(self): 89 | # we can access object attributes and other methods 90 | # using the self variable 91 | print(self.name) 92 | 93 | # but we can't access variables defined outside the current method 94 | # if you uncomment the statement below it will raise 95 | # NameError: name 'ime' is not defined 96 | #print(ime) 97 | 98 | 99 | NOTE: When testing in Python we don't instantiate (create objects) from the 100 | test classes by hand. The test runner does this for us automatically when the 101 | `unittest.main()` statement is executed in the main block. 102 | 103 | 104 | More general information about OOP can be found in [module04](../module04) of this guide! 105 | 106 | 107 | ## Tasks & homework 108 | 109 | * Examine the `calculator.py` program and 110 | * write tests for all of its functions. Use the 111 | `test_calculator.py::CalculatorTestCase::test_` naming scheme for your 112 | test methods! 113 | * Modify `calculator.py` to avoid executing the interactive commands when testing 114 | * **TIP**: use `test.py` to verify that your tests are correct! 115 | 116 | 117 | * Examine the existing test suite (for all previous tasks) in details. What other tests can be 118 | written ? Write them! 119 | -------------------------------------------------------------------------------- /module05/calculator.py: -------------------------------------------------------------------------------- 1 | ''' Program make a simple calculator that can add, subtract, multiply and divide using functions ''' 2 | 3 | # This function adds two numbers 4 | def add(x, y): 5 | return x + y 6 | 7 | # This function subtracts two numbers 8 | def subtract(x, y): 9 | return x - y 10 | 11 | # This function multiplies two numbers 12 | def multiply(x, y): 13 | return x * y 14 | 15 | # This function divides two numbers 16 | def divide(x, y): 17 | return x / y 18 | 19 | print("Select operation.") 20 | print("1.Add") 21 | print("2.Subtract") 22 | print("3.Multiply") 23 | print("4.Divide") 24 | 25 | # Take input from the user 26 | choice = int(input("Enter choice(1/2/3/4):")) 27 | 28 | num1 = int(input("Enter first number: ")) 29 | num2 = int(input("Enter second number: ")) 30 | 31 | if choice == 1: 32 | print(num1,"+",num2,"=", add(num1,num2)) 33 | elif choice == 2: 34 | print(num1,"-",num2,"=", subtract(num1,num2)) 35 | elif choice == 3: 36 | print(num1,"*",num2,"=", multiply(num1,num2)) 37 | elif choice == 4: 38 | print(num1,"/",num2,"=", divide(num1,num2)) 39 | else: 40 | print("Invalid choice", choice) 41 | -------------------------------------------------------------------------------- /module05/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import unittest.mock 4 | from io import StringIO 5 | 6 | import test_calculator 7 | 8 | class TestCalculatorTestCase(unittest.TestCase): 9 | def setUp(self): 10 | """ 11 | Will fail if the test_calculator module doesn't have a 12 | CalculatorTestCase class defined! 13 | """ 14 | self.test_case_class = test_calculator.CalculatorTestCase 15 | self.calculator_functions = ['add', 'subtract', 'multiply', 'divide'] 16 | 17 | def test_attributes_exist(self): 18 | """ 19 | Validate that all test methods for the calculator.py program 20 | follow the naming convention test_ 21 | """ 22 | for attr in self.calculator_functions: 23 | self.assertTrue(hasattr(self.test_case_class, 'test_%s' % attr)) 24 | 25 | def test_calculator_tests_must_pass(self): 26 | """ 27 | Validate that when executed the test cases for the calculator 28 | will actually PASS. We do this by executing them. 29 | """ 30 | suite = unittest.TestSuite() 31 | for attr in self.calculator_functions: 32 | suite.addTest(self.test_case_class('test_%s' % attr)) 33 | 34 | result = unittest.TestResult() 35 | 36 | suite.run(result) 37 | self.assertTrue(result.wasSuccessful()) 38 | 39 | def test_calculator_tests_call_expected_calculator_functions(self): 40 | """ 41 | Validate that calculator tests will actually execute 42 | the expected functions from the calculator module. We do this 43 | by mocking the functions, executing the test methods and 44 | verifying that the test method actually called the mocked 45 | function! 46 | """ 47 | for attr in self.calculator_functions: 48 | with unittest.mock.patch('test_calculator.calculator.%s' % attr) as calc_func: 49 | t = self.test_case_class('test_%s' % attr) 50 | result = t.run() 51 | 52 | # NOTE: we should get an error like this 53 | # AssertionError: 5 != 54 | # when executing the calculator test methods 55 | self.assertFalse(result.wasSuccessful()) 56 | 57 | # however we make sure that all functions in the calculator module 58 | # have actually been called 59 | self.assertTrue(calc_func.called) 60 | 61 | def test_calculator_interactive_not_executed_when_imported(self): 62 | """ 63 | Verify that the interactive commands are not executed when 64 | the calculator module is imported. We do this by mocking 65 | the input function to deal away with interactiveness in the test 66 | and mocking sys.stdout to assert that nothing was printed! 67 | """ 68 | if 'calculator' in sys.modules: 69 | # force import of the calculator module 70 | # b/c already imported from test_calculator 71 | del sys.modules['calculator'] 72 | 73 | with unittest.mock.patch('sys.stdout', new_callable=StringIO) as _stdout, \ 74 | unittest.mock.patch('builtins.input', return_value=1) as _input: 75 | import calculator 76 | # print was never called 77 | self.assertEqual('', _stdout.getvalue().strip()) 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /module05/test_example.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime 3 | 4 | class ExampleTestCase(unittest.TestCase): 5 | @classmethod 6 | def setUpClass(cls): 7 | print("setUpClass executes only once. CREATE DATABASE;") 8 | 9 | def setUp(self): 10 | print(" setUp executes before every test method!") 11 | 12 | def tearDown(self): 13 | print(" tearDown executes after every test method!") 14 | 15 | @classmethod 16 | def tearDownClass(cls): 17 | print("tearDownClass executes only once. DROP DATABASE;") 18 | 19 | def test_first_method(self): 20 | print(" This is the first test. It will PASS") 21 | assert True 22 | 23 | def test_second_method(self): 24 | print(" This is the second test. It will FAIL") 25 | assert False 26 | 27 | def test_zzzzz(self): 28 | print(" This is the last test. It will PASS") 29 | assert True 30 | 31 | 32 | class FlakyTest(unittest.TestCase): 33 | ''' 34 | Execute this several times quickly to make it fail! 35 | ''' 36 | def do_fail(self): 37 | if datetime.now().second % 3 == 0: 38 | raise Exception('I am a flaky test') 39 | 40 | def test_myself(self): 41 | self.do_fail() 42 | 43 | if __name__ == "__main__": 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /module06/README.md: -------------------------------------------------------------------------------- 1 | # Module 06: Selenium with Python 2 | 3 | ## Preparation: 4 | 5 | Read the 6 | [Unofficial Selenium Python documentation](http://selenium-python.readthedocs.io/); 7 | 8 | ## Installation 9 | 10 | 1. Download the .tar.gz archive from https://pypi.python.org/pypi/selenium and 11 | extract the `selenium/` directory into the current directory (or anywhere on the 12 | Python module search path) 13 | 2. Download geckodriver from https://github.com/mozilla/geckodriver/releases and 14 | extract `geckodriver` or `geckodriver.exe` under search path. If Selenium is not 15 | able to find `geckodriver` automatically then use: 16 | 17 | driver = webdriver.Firefox(executable_path = 'C:\\geckodriver.exe') 18 | 19 | ## Selenium WebDriver basics 20 | 21 | 1. Driver initialization 22 | 23 | from selenium import webdriver 24 | driver = webdriver.Firefox() 25 | 26 | 2. Navigation: 27 | 28 | driver.get(“http://google.com”) 29 | 30 | 31 | 3. Locating elements, see 32 | [Chapter 4. Locating elements](http://selenium-python.readthedocs.io/locating-elements.html) 33 | 34 | driver.find_element_by_id('loginForm') 35 | driver.find_elements_by_id('tweetMeButton') 36 | 37 | 4. Actions: 38 | 39 | from selenium.webdriver.common.keys import Keys 40 | element.send_keys("some text") 41 | element.send_keys(" and then some more", Keys.RETURN) 42 | anyElement.click() 43 | 44 | # hover element 45 | from selenium.webdriver.common.action_chains import ActionChains 46 | ActionChains(driver).move_to_element(element).perform() 47 | 48 | # get element values 49 | print element.text 50 | print element.get_attribute('attr_name') 51 | 52 | 5. Waits, see [Chapter 5. Waits](http://selenium-python.readthedocs.io/waits.html): 53 | 54 | - Implicit Waits 55 | 56 | This means to poll the DOM for a certain amount of time when trying to find an element 57 | if they are not immediately available. The default setting is 0. Once set, the implicit 58 | wait is set for the life of the WebDriver object instance. 59 | 60 | 61 | from selenium import webdriver 62 | 63 | driver = webdriver.Firefox() 64 | driver.implicitly_wait(10) # seconds 65 | driver.get("http://somedomain/url_that_delays_loading") 66 | myDynamicElement = driver.find_element_by_id("myDynamicElement") 67 | 68 | 69 | - Explicit Waits 70 | 71 | An explicit wait is code you define to wait for a certain condition to occur before proceeding 72 | further in the code. The worst case of this is `time.sleep()`, which sets the condition to an 73 | exact time period to wait. There are some convenience methods provided that help you write code 74 | that will wait only as long as required. `WebDriverWait` in combination with `expected_conditions` 75 | is one way this can be accomplished. 76 | 77 | from selenium import webdriver 78 | from selenium.webdriver.common.by import By 79 | from selenium.webdriver.support.ui import WebDriverWait 80 | from selenium.webdriver.support import expected_conditions as EC 81 | 82 | driver = webdriver.Firefox() 83 | driver.get("http://somedomain/url_that_delays_loading") 84 | try: 85 | element = WebDriverWait(driver, 10).until( 86 | EC.presence_of_element_located((By.ID, "myDynamicElement")) 87 | ) 88 | finally: 89 | driver.quit() 90 | 91 | - How to get Selenium to wait for page load after a click 92 | 93 | See this 94 | [blog post](http://www.obeythetestinggoat.com/how-to-get-selenium-to-wait-for-page-load-after-a-click.html). 95 | 96 | 97 | 6. Handling alerts, see 98 | [Chapter 7.3 Alerts](http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.alert) 99 | 100 | 101 | simpleAlert = driver.switch_to_alert() 102 | alertText = simpleAlert.text 103 | simpleAlert.accept() 104 | 105 | 106 | 7. Ending test session/execution: 107 | - `driver.close()` – closes the current browser instance; does not destroy the driver object 108 | - `driver.quit()` – closes the current browser instance and destroys the driver object; 109 | Once `driver.quit()` is called, the driver object can’t be used (needs to be re-initialized) 110 | 111 | 8. Common exceptions, see 112 | [Chapter 7.1 Exceptions](http://selenium-python.readthedocs.io/api.html#module-selenium.common.exceptions) 113 | - `NoSuchElementException` – when no element with the given address exists in the DOM 114 | - `ElementNotVisibleException` – when an element with the given address exists in the DOM, but is, for some reason, hidden 115 | 116 | 9. Asserts 117 | 118 | **NOTE:** most Selenium bindings, including these for Python don't have handy asserts to verify 119 | existence of elements, number of children elements or particular properties. You will have to 120 | locate the required elements and use the base `assert ` statement or assert 121 | methods which come from your unit testing framework of choice. Possibly couple that with iterating 122 | over the selected element to inspect its children, etc! 123 | 124 | 10. Taking Screenshots 125 | 126 | driver.save_screenshot('screenshot.png') 127 | 128 | import codecs 129 | codecs.open('my-page.html', "w", "utf-8").write(driver.page_source) 130 | 131 | 11. Handling iframes 132 | 133 | See http://selenium-python.readthedocs.io/navigating.html#moving-between-windows-and-frames 134 | 135 | driver.switch_to_frame("frameName") 136 | driver.switch_to_frame("frameName.0.child") 137 | driver.switch_to_default_content() 138 | 139 | For non-Python examples see http://toolsqa.com/selenium-webdriver/handling-iframes-using-selenium-webdriver/ 140 | 141 | 142 | ## Tasks 143 | 144 | Selenium is a generic browser automation engine. That means it can be used not 145 | only during testing but also in regular programs. Create a program called 146 | `solution.py` which 147 | 148 | * Navigates to the [BBC Weather page for Sofia](http://www.bbc.com/weather/727011) 149 | * implements a function `get_weather(driver, days)` which returns the weather conditions for 150 | a given number of days. `driver` is a Selenium driver object and `days` is the number of days 151 | for which to return results. 152 | * The return type is a list containing tuples of type (str, int). 153 | The first element in the tuple is the human readable name of the weather, e.g. *Sunny*, 154 | *Thundery Shower*, etc and the second element is the maximum temperature 155 | * If `days` is not a valid value then return None 156 | 157 | Use `test.py` to validate your solution is correct. 158 | -------------------------------------------------------------------------------- /module06/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from selenium import webdriver 3 | 4 | import solution 5 | 6 | class BBCWeatherTests(unittest.TestCase): 7 | @classmethod 8 | def setUpClass(cls): 9 | cls.driver = webdriver.Firefox() 10 | 11 | @classmethod 12 | def tearDownClass(cls): 13 | cls.driver.close() 14 | try: 15 | cls.driver.quit() 16 | except: 17 | pass 18 | 19 | def test_get_weather_minus_1(self): 20 | self.assertEqual(None, solution.get_weather(self.driver, -1)) 21 | 22 | def test_get_weather_0(self): 23 | self.assertEqual(None, solution.get_weather(self.driver, 0)) 24 | 25 | def test_get_weather_11(self): 26 | self.assertEqual(None, solution.get_weather(self.driver, 11)) 27 | 28 | 29 | def test_get_weather_5_for_sofia(self): 30 | self.driver.get('http://www.bbc.com/weather/727011') 31 | result = solution.get_weather(self.driver, 5) 32 | 33 | day_values = self.driver.find_elements_by_class_name('daily__day-tab') 34 | for i in range(len(result)): 35 | day = day_values[i] 36 | name, temp = result[i] 37 | image = day.find_element_by_xpath('//span[@title="%s"]' % name) 38 | self.assertTrue(image is not None) 39 | self.assertTrue("%s°C" % temp in day.text) 40 | 41 | 42 | def test_get_weather_10_for_new_york(self): 43 | self.driver.get('http://www.bbc.com/weather/5128581') 44 | result = solution.get_weather(self.driver, 10) 45 | 46 | day_values = self.driver.find_elements_by_class_name('daily__day-tab') 47 | for i in range(len(result)): 48 | day = day_values[i] 49 | name, temp = result[i] 50 | image = day.find_element_by_xpath('//span[@title="%s"]' % name) 51 | self.assertTrue(image is not None) 52 | self.assertTrue("%s°C" % temp in day.text) 53 | 54 | 55 | if __name__ == "__main__": 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /module07/README.md: -------------------------------------------------------------------------------- 1 | # Module 07: Page Objects design pattern 2 | 3 | ## Preparation: 4 | 5 | Read 6 | [Chapter 6. Page Objects](http://selenium-python.readthedocs.io/page-objects.html); 7 | 8 | ## Page object basics 9 | 10 | Page Objects are Python classes which encapsulate page behavior and structure. 11 | This allows a layer of separation between tests and actual page implementation. 12 | 13 | Instructor will explain the example from the tutorial and answer questions 14 | posted by the students. 15 | 16 | ## Tasks 17 | 18 | Go back to the tasks from Module 06 and rewrite them using Page Objects. 19 | Discuss your findings and issues with the instructor. Instructor will examine 20 | the resulting test scripts and provide feedback. 21 | 22 | -------------------------------------------------------------------------------- /module07/examples/pages.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.keys import Keys 2 | 3 | class BasePage(object): 4 | def __init__(self, driver): 5 | self.driver = driver 6 | 7 | class MainPage(BasePage): 8 | # method override 9 | def __init__(self, driver): 10 | # call parent class method 11 | super(MainPage, self).__init__(driver) 12 | # navigate to the correct URL 13 | self.driver.get('http://python.org') 14 | 15 | def search_for(self, find_me): 16 | search_field = self.driver.find_element_by_id('id-search-field') 17 | search_field.send_keys(find_me, Keys.RETURN) 18 | 19 | 20 | class SearchResults(BasePage): 21 | # helper variable to optimize HTML elements search 22 | _results = None 23 | 24 | # variant 1 with optimization 25 | # def results(self): 26 | # if self._results is None: 27 | # self._results = [] 28 | # 29 | # main_content = self.driver.find_element_by_class_name('main-content') 30 | # ul = main_content.find_element_by_css_selector('form > ul') 31 | # assert ul != None 32 | # for a in ul.find_elements_by_tag_name('a'): 33 | # self._results.append(a) 34 | # 35 | # return self._results 36 | 37 | 38 | # variant 2 w/o optimization 39 | def results(self): 40 | R = [] 41 | 42 | main_content = self.driver.find_element_by_class_name('main-content') 43 | ul = main_content.find_element_by_css_selector('form > ul') 44 | assert ul != None 45 | for a in ul.find_elements_by_tag_name('a'): 46 | print a.text 47 | R.append(a) 48 | 49 | return R 50 | 51 | -------------------------------------------------------------------------------- /module07/examples/test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pages 3 | import unittest 4 | from selenium import webdriver 5 | 6 | class PageObjectTestCase(unittest.TestCase): 7 | def setUp(self): 8 | self.driver = webdriver.Firefox() 9 | 10 | def tearDown(self): 11 | self.driver.quit() 12 | 13 | def test_search_for_tutorial_should_work(self): 14 | # create main page and navigate to it 15 | main_page = pages.MainPage(self.driver) 16 | main_page.search_for('tutorial') 17 | 18 | # wait for search results to load 19 | time.sleep(7) 20 | search_results = pages.SearchResults(self.driver) 21 | # .results is a list of tags 22 | assert search_results.results()[0].get_attribute('href') == 'https://www.python.org/doc/newstyle' 23 | assert search_results.results()[0].text == 'New-style Classes' 24 | 25 | results = search_results.results() 26 | assert results[0].text == 'New-style Classes' 27 | 28 | 29 | def test_search_for_non_existing(self): 30 | # create main page and navigate to it 31 | main_page = pages.MainPage(self.driver) 32 | main_page.search_for('banitza') 33 | 34 | # wait for search results to load 35 | time.sleep(7) 36 | search_results = pages.SearchResults(self.driver) 37 | # .results is empty 38 | assert 0 == len(search_results.results()) 39 | 40 | 41 | if __name__ == '__main__': 42 | unittest.main() 43 | 44 | -------------------------------------------------------------------------------- /module08/README.md: -------------------------------------------------------------------------------- 1 | # Module 08: Writing automated tests for real scenarios 2 | 3 | ## Preparation: 4 | 5 | Students need to prepare 3 to 5 real-world test scenarios. 6 | These scenarios need to be defined in enough details so that it is 7 | evident what actions need to be executed and how the resulting conditions 8 | will be tested! 9 | 10 | 11 | Please use 12 | [Given-When-Then](https://github.com/cucumber/cucumber/wiki/Given-When-Then) 13 | to describe your scenarios. 14 | 15 | **TIPS:** 16 | - define small scenrios, e.g. visit a web page, perform only 1 action 17 | and inspect the results 18 | - Sites that can be used for testing: 19 | - http://shop.pragmatic.bg 20 | - https://mozillians.org 21 | - Open source projects which could use your help 22 | - [Kiwi TCMS](http://kiwitcms.org) - provides docker compose to run the 23 | application. Needs lots of Selenium based tests! 24 | 25 | 26 | ## Tasks 27 | 28 | * Start from the easiest scenario and write automated test using Selenium 29 | * Continue automating the rest of you scenarios 30 | -------------------------------------------------------------------------------- /workshop/README.md: -------------------------------------------------------------------------------- 1 | # QA automation with Python & Selenium Workshop 2 | 3 | ## Environment setup 4 | 5 | Students need to prepare the following environment on their computers: 6 | 7 | * Install Python 3 on your computer from https://www.python.org/downloads/; 8 | * Text editor of choice, preferably [PyCharm IDE](https://www.jetbrains.com/pycharm/); 9 | * Selenium & geckodriver, see [Selenium installation](../module06/#installation) 10 | 11 | ## Rules of engagement 12 | 13 | * Each module has links and instructions for preparation. It is best to 14 | read all of these before the workshop; 15 | * Workshop will explain the theory from the preparation section 16 | and focus on writing programs to solidify the knowledge; 17 | * Each module is scheduled to take 1 hour; 18 | * You have 3 lives for the entire workshop. If you don't complete 19 | the tasks for the given module you lose 1 life. When all of your 3 lives are lost 20 | it is **GAME OVER**! 21 | 22 | 23 | **NOTES:** 24 | 25 | * Students should be prepared beforehand since the 26 | workshop does not intend to focus on installation related issues! 27 | * Students must be familiar with their tools of choice, e.g. they need to know 28 | how to use the IDE or text editor, how to search files, how to search for 29 | particular text, etc; 30 | * Instructor may be using different text editor and operating system and may not be 31 | able to help with matters related to any particular tool if they are not obvious 32 | enough; 33 | * Students are advised to use the same tools so they can help each other; 34 | 35 | Useful links: 36 | 37 | * [Dive into Python](http://www.diveintopython.net/) 38 | * [The Python Tutorial](https://docs.python.org/3/tutorial/index.html) 39 | * [Official Python documentation](https://docs.python.org/) 40 | * [Unofficial Selenium Python documentation](http://selenium-python.readthedocs.io/) 41 | * [Official Selenium API documentation for Python](https://seleniumhq.github.io/selenium/docs/api/py/api.html) 42 | 43 | 44 | ## Hour 01: [Structure of a Python program, functions](../module01/) 45 | ## Hour 02: [Data types and structures](../module02/) 46 | ## Hour 03: [If statements and loops](../module03/) 47 | ## Hour 04: [Testing in Python and OOP for testers](../module05/) 48 | ## Hour 05: [Selenium with Python](../module06/) 49 | ## Hour 06: [Stand-alone work: writing automated tests](../module08/) 50 | 51 | All materials here are licensed under CC-BY-SA license, see 52 | https://creativecommons.org/licenses/by-sa/4.0/ for more information. 53 | --------------------------------------------------------------------------------