Full width home advertisement

Post Page Advertisement [Top]

Lecture 7 - CS50's Web Programming with Python and JavaScript

Testing

Testing

One important part of the software development process is the act of Testing the code we've written to make sure everything runs as we expect it to.

Assert

> Use 'assert' command to run tests in Python
- This command is followed by some expression that should be 'True'.
- If the expression is 'True', nothing will happen, and if it is 'False', an exception will be thrown. 

- How we could incorporate command to test the 'square' function we wrote. When the function is written correctly, nothing happens as the 'assert' is 'True'. 
def square(x):
    return x * x

assert square(10) == 100

""" Output:

"""

- When it is written incorrectly, an exception is thrown. 
def square(x):
    return x + x

assert square(10) == 100

""" Output:
Traceback (most recent call last):
  File "assert.py", line 4, in <module>
    assert square(10) == 100
AssertionError
"""

> So one way we can test our code is by including a number of these different assert statements. For complex functions that have multiple different conditional branches, being able to assert that no matter which conditional branch the program chooses to follow that the code will actually be correct can be a valuable thing to be able to say. 
- This can be helpful too when in working on a larger project, deal with the problem of bugs that might appear inside of a project. 
- These tests can start to grow over time. As you continue working on your project, you can always run those existing set of tests to make sure that nothing- no changes that you make to the program down the line, no future features that you add or changes you might make are going to break anything that was there before. 
- So assert is one basic way of just saying that I would like for this statement to be true. If it's not true, throw an exception. 

Test-Driven Development

As you begin building larger projects, consider using test-driven development, a development style where every time you fix a bug, you add a test that checks for that bug to a growing set of tests that are run every time you make changes. 
- This will help you to make sure that additional features you add to a project don't interfere with your existing features. 

> Write a function called 'is_prime' that returns 'True' if and only if its input is prime:
import math

def is_prime(n):

    # We know numbers less than 2 are not prime
    if n < 2:
        return False

    # Checking factors up to sqrt(n)
    for i in range(2, int(math.sqrt(n))):

        # If i is a factor, return false
        if n % i == 0:
            return False

    # If no factors were found, return true
    return True
- Square root doesn't already happen to be an integer. 

- Type 'python' in the terminal and test the function to make sure that it works the way that I would want it to work. 


> A function to test our 'prime' function:
from prime import is_prime

def test_prime(n, expected):
    if is_prime(n) != expected:
        print(f"ERROR on is_prime({n}), expected {expected}")

> At this point, we can go into our Python interpreter and test out some values:
>>> test_prime(5, True)
>>> test_prime(10, False)
>>> test_prime(25, False)
ERROR on is_prime(25), expected False

- test_prime(5, True): Nothing happens, means everything was okay. If there were an error, it would've printed something out.  
- test_prime(25, False): My program probably returned True and thinks that 25 is a prime number. 

> We can see from the output above that 5 and 10 were correctly identified as prime and not prime, but 25 was incorrectly identified as prime, so there must be something wrong with our function. 
- A way to automate our testing - One way we can do this is by creating a shell script, or some script that can be run inside our terminal. 
- These files require a '.sh' extension, so the file will be called 'tests0.sh'.

- Each of the lines below consists of
1. A 'python3' to specify the Python version we're running
2. A '-c' to indicate that we wish to run a command
3. A command to run in string format

- Rather than have to run each test one at a time, just run tests0.sh. 
python3 -c "from tests0 import test_prime; test_prime(1, False)"
python3 -c "from tests0 import test_prime; test_prime(2, True)"
python3 -c "from tests0 import test_prime; test_prime(8, False)"
python3 -c "from tests0 import test_prime; test_prime(11, True)"
python3 -c "from tests0 import test_prime; test_prime(25, False)"
python3 -c "from tests0 import test_prime; test_prime(28, False)"
+ (Windows CMD): Use .bat file.
1. Make a file 'tests0.bat' in VSC 

> Now we can run these commands by running './tests0.sh' in our terminal, the result:
ERROR on is_prime(8), expected False
ERROR on is_prime(25), expected False

> Modify python3 in tests0.sh file to python, test in Git Bash


Unit Testing

Even though we were able to run tests automatically using the above method, we want to avoid having to write out each of those tests. 

> Use the Python 'unittest' library to make this process a little bit easier.
- A testing program might look like for our 'is_prime' function.
# Import the unittest library and our function
import unittest
from prime import is_prime

# A class containing all of our tests
class Tests(unittest.TestCase):

    def test_1(self):
        """Check that 1 is not prime."""
        self.assertFalse(is_prime(1))

    def test_2(self):
        """Check that 2 is prime."""
        self.assertTrue(is_prime(2))

    def test_8(self):
        """Check that 8 is not prime."""
        self.assertFalse(is_prime(8))

    def test_11(self):
        """Check that 11 is prime."""
        self.assertTrue(is_prime(11))

    def test_25(self):
        """Check that 25 is not prime."""
        self.assertFalse(is_prime(25))

    def test_28(self):
        """Check that 28 is not prime."""
        self.assertFalse(is_prime(28))


# Run each of the testing functions
if __name__ == "__main__":
    unittest.main()

: Each of the functions within our 'Tests' class followed a pattern
- The name of the functions begin with 'test_'. This is necessary for the functions to be run automatically with the call to 'unittest.main()'.
- Each test takes in the 'self' argument. This is standard when writing methods within Python classes. 
- The first line of each function contains a docstring(""") surrounded by three quotation marks. These aren't just for the code's readability. When the tests are run, the comment will be displayed as a description of the test if it fails. 
- The next line of each of the functions contained an assertion in the form 'self.assertSOMETHING'. There are many different assertions you can including 'assertTrue', 'assertFalse', 'assertEqual', 'assertGreater'. 

(unittest.TestCase): This is going to be a class that is going to define a whole bunch of functions, each of which is something that I would like to test. 
self.assertFalse(is_prime(1)): I would like to assert that 'is_prime(1) is false.
if __name__ == "__main": unittest.main() If you run the program, call 'unittest.main', which will run all of these unit tests. 

(terminal) python tests1.py

> The results of these tests:
...F.F
======================================================================
FAIL: test_25 (__main__.Tests)
Check that 25 is not prime.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests1.py", line 26, in test_25
    self.assertFalse(is_prime(25))
AssertionError: True is not false

======================================================================
FAIL: test_8 (__main__.Tests)
Check that 8 is not prime.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests1.py", line 18, in test_8
    self.assertFalse(is_prime(8))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (failures=2)



- After running the tests, 'unittest' provides us with some useful information about what it found. In the 1st line, it gives us a series of '.'s for successes and 'F's for failures in the order our tests were written.
...F.F

- Next, for each of the tests that failed, we are then given the name of the function that failed:
FAIL: test_25 (__main__.Tests)

- The descriptive comment we provided earlier:
Check that 25 is not prime.

- A traceback for the exception:
Traceback (most recent call last):
  File "tests1.py", line 26, in test_25
    self.assertFalse(is_prime(25))
AssertionError: True is not false

- Finally, we are given a run through of how many tests were run, how much time they took, and how many failed.
Ran 6 tests in 0.001s

FAILED (failures=2)


> Fix the bug in our function. We need to test one additional number in our 'for' loop. For example, when 'n' is '25', the square root is '5', but when that is one argument in the 'range' function, the 'for' loop terminates at the number 4. Therefore, we can simply change the header of our 'for' loop to:
for i in range(2, int(math.sqrt(n)) + 1):
- 25: n=25일 때 range(2, 6)으로 25%5==0을 확인 가능
- 8: n=8일 때 루트8은 약 2.828, int(math.sqrt(8))=2, range(2, 2)는 반복 0번 => return True

- When we run the tests again using our unit tests, we get the following output, indicating that our change fixed the bug. 
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK


- These automated tests will become more useful as you work to optimize this function. 
- For example, you might want to use the fact that you don't need to check all integers as factors, just smaller primes (if a number is not divisible by 3, it is also not divisible by 6, 9, 12, ...), or you may want to use more advanced probabilistic primality tests such as the Fermat and Miller-Rabin primality tests.
- Whenever you make changes to improve this function, you'll want the ability to easily run your unit tests again to make sure your function is still correct. 

Django Testing

> How we can apply the ideas of automated testing when creating Django applications. 
- We'll be using the 'flights' project we created when we first learned about Django models. 
- First, add a method to our 'Flight' model that verifies that a flight is valid by checking for two conditions:
1. The origin is not the same as the destination
2. The duration is greater than 0 minutes

> Our model look like 
class Flight(models.Model):
    origin = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="departures")
    destination = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="arrivals")
    duration = models.IntegerField()

    def __str__(self):
        return f"{self.id}: {self.origin} to {self.destination}"

    def is_valid_flight(self):
        return self.origin != self.destination or self.duration > 0

> Whenever we create a new application, we're automatically given a 'tests.py' file. When we first open this file, we see that Django's TestCase library is automatically imported:
from django.test import TestCase

> The advantage to using the 'TestCase' library is that when we run our tests, an entirely new database will be created for testing purposes only. 
- We avoid the risk of accidentally modifying or deleting existing entries in our database and we don't have to worry about removing dummy entries that we created only for testing.
- To start using this library, import all of our models:
from .models import Flight, Airport, Passenger

> Create a new class that extends the 'TestCase' class we just imported.
- class FlightTestCase: Will define all of the tests that I would like to run on my flight's application
- Within this class, we'll define a 'setUp' function that will be run at the start of the testing process. 
- In this function, we'll probably want to create.
class FlightTestCase(TestCase):

    def setUp(self):

        # Create airports.
        a1 = Airport.objects.create(code="AAA", city="City A")
        a2 = Airport.objects.create(code="BBB", city="City B")

        # Create flights.
        Flight.objects.create(origin=a1, destination=a2, duration=100)
        Flight.objects.create(origin=a1, destination=a1, duration=200)
        Flight.objects.create(origin=a1, destination=a2, duration=-100)

> Add some functions to this class to perform some tests.
- First, make sure our 'departures' and 'arrivals' fields work correctly by attempting to count the number of departures and arrivals from airport 'AAA':
def test_departures_count(self):
    a = Airport.objects.get(code="AAA")
    self.assertEqual(a.departures.count(), 3)

def test_arrivals_count(self):
    a = Airport.objects.get(code="AAA")
    self.assertEqual(a.arrivals.count(), 1)
def test_departures_count(self):: Every airport has access to a field called departures, which ideally should be how many flights are departing from that airport. 
self.assertEqual(a.departures.count(), 3): If I take airport a and count how many flights are departing from that airport, that should be 3, so just verifying that works.
- 'assertTrue' verifies if something is true / 'assertFalse' verifies if something is false / 'assertEqual' verifies that two numbers are equal to each other. 
- After that, if this test passes, then I can be confident that elsewhere in my program, if I take an airport and call that 'airport.departures.count', I can feel confident that that is going to work the way I would expect it to. 

def test_arrivals_count(self):: Do the same thing for arrivals. Get the airport, and assert that 'a.arrivals.count' that that is going to be equal to the number 1 if there's only one flight that arrives at airport a1, for example. 


> Also test the 'is_valid_flight' function we added to the 'Flight' model. 
- Begin by asserting that the function does return true when the flight is valid:
def test_valid_flight(self):
    a1 = Airport.objects.get(code="AAA")
    a2 = Airport.objects.get(code="BBB")
    f = Flight.objects.get(origin=a1, destination=a2, duration=100)
    self.assertTrue(f.is_valid_flight())
def test_valid_flight(self):: Get two airports a1 and a2. And get the flight whose origin is a1, destination is a2, duration is 100. 
- 'assertTrue' that this flight is going to be a valid flight because this flight is valid. The origin is different from the destination, its duration is some positive number of minutes. 


> Make sure that flights with invalid destinations and durations return false:
def test_invalid_flight_destination(self):
    a1 = Airport.objects.get(code="AAA")
    f = Flight.objects.get(origin=a1, destination=a1)
    self.assertFalse(f.is_valid_flight())

def test_invalid_flight_duration(self):
    a1 = Airport.objects.get(code="AAA")
    a2 = Airport.objects.get(code="BBB")
    f = Flight.objects.get(origin=a1, destination=a2, duration=-100)
    self.assertFalse(f.is_valid_flight())
def test_invalid_flight_destination(self):: Do the same thing for testing for an invalid flight, because the destination is bad. Get the flightairport a1, and get the flight whose origin and destination are both a1. 
self.assertFalse(f.is_valid_flight) : Say that this should not be a valid flight becuse the origin and the destination are the same.
def test_invalid_flight_duration(self): : A flight can be invalid because of its duration. Get airports a1 and a2. And get me the flight whose origin is a1, destination is a2, but the duration is negative 100 minutes. 
self.assertFalse(f.is_valid_flight()) : That should not be a valid flight. When I call 'is _valid_flight' on that flight, it shouldn't be valid because the duration makes it an invalid flight. 


> Run 'python manage.py test' to run our tests.
- This output for this is almost identical to the output we saw while using the Python 'unittest' library, although it also logs that it is creating and destroying a testing database:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..FF.
======================================================================
FAIL: test_invalid_flight_destination (flights.tests.FlightTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/cleggett/Documents/cs50/web_notes_files/7/django/airline/flights/tests.py", line 37, in test_invalid_flight_destination
    self.assertFalse(f.is_valid_flight())
AssertionError: True is not false

======================================================================
FAIL: test_invalid_flight_duration (flights.tests.FlightTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/cleggett/Documents/cs50/web_notes_files/7/django/airline/flights/tests.py", line 43, in test_invalid_flight_duration
    self.assertFalse(f.is_valid_flight())
AssertionError: True is not false

----------------------------------------------------------------------
Ran 5 tests in 0.018s

FAILED (failures=2)
Destroying test database for alias 'default'...



> We can see from the output that there are times when 'is_valid_flight' returned 'True' when it should have returned 'False'. 
- Upon further inspection of our function, that we made the mistake of using 'or' instead of 'and', meaning that only one of the flight requirements must be filled for the flight to be valid. 
- If we change the function to this:
 def is_valid_flight(self):
    return self.origin != self.destination and self.duration > 0

> Run the tests again with better results:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.014s

OK
Destroying test database for alias 'default'...


+ is_valid_flight에서
1. ori와 dest가 다를 때 valid임
2. du가 0보다 클 때 valid임 
즉, 1과 2 둘 다 만족할 때 valid임 / 하나라도 틀리면 invalid
그럼 ori랑 dest랑 같은 flight랑 du가 마이너스인 flight들은 하나라도 틀린 상태라 invalid가 됨
즉 assertFalse가 맞음 / false인 f을 assertFalse하면 false==false니까 AssertionError가 안 나오게 되는 것

Client Testing

While creating web applications, we want to check whether or not individual web pages load as intended. 
> We can do this by creating a 'Client' object in our Django testing class, and then making requests using that object. 
- First, add 'Client' to our imports:
from django.test import Client, TestCase

> For example, add a test that makes sure that we get an HTTP response code of 200 and that all three of our flights are added to the context of a response:
def test_index(self):

    # Set up client to make requests
    c = Client()

    # Send get request to index page and store response
    response = c.get("/flights/")

    # Make sure status code is 200
    self.assertEqual(response.status_code, 200)

    # Make sure three flights are returned in the context
    self.assertEqual(response.context["flights"].count(), 3)
def test_index(self): : Test my default flights page to make sure that it works correctly.
c = Client() : Create a client, some is going to be interacting request and response style.
response = c.get("/flights") : The route that gets me the index page for all the flights. I get response back from trying to get that page, save inside of the variable 'response'.
self.assertEqual(response.status_code, 200) : Whatever response is, make sure that is going to be a 200. If there was some sort of error, like a 404 or a 500, I'd like to know about that. 
self.assertEqual(response.context["flights"].count(), 3) : In Django, when we rendered a template, we called return render then provided the request and what page we were going to render. We could also provide some context, some Python dictionary, describing all of the values that we wanted to pass in to that template. Django's testing framework gives us access to that context so that we can test to make sure that it contains what we would expect it to contain. / I expect that to contain a listing of all of the flights. 'response.context["flights"] gets me whatever was passed in as flights in the context, I want to make sure that there are 3 results.


> We can check to make sure we get a valid response code for valid flight page, and an invalid response code for a flight page that doesn't exist. 
* We use the 'Max' function to find the maximum 'id', which we have access to by including 'from django.db.models import Max' at the top of the file.
def test_valid_flight_page(self):
    a1 = Airport.objects.get(code="AAA")
    f = Flight.objects.get(origin=a1, destination=a1)

    c = Client()
    response = c.get(f"/flights/{f.id}")
    self.assertEqual(response.status_code, 200)

def test_invalid_flight_page(self):
    max_id = Flight.objects.all().aggregate(Max("id"))["id__max"]

    c = Client()
    response = c.get(f"/flights/{max_id + 1}")
    self.assertEqual(response.status_code, 404)
def test_valid_flight_page(self): : Get a particular flight who had an origin a1 and a destination a1, that it wasn't a valid flight. 
response = c.get(f"/flights/{f.id}") : On my flight's page, I'd like to be able to go to /flights/1 to get at flight number 1. So if I take some valid ID, some ID of an actual flight f and go to /flights/.id should work. It should have a status code of 200. 

max_id = Flight.objects.all().aggregate(Max("id"))["id__max"] : A Django command that will get me the maximum value for the ID. 'id__max' gets me the biggest possible ID out of all of the flights that happen to exist inside of my database. 
response = c.get(f"/flights/{max_id + 1}") : Get '/flights/{max_id+1}' so a number that is 1 greater than any of the flights that were already inside of my database, that shouldn't work. There shouldn't be a page for a flight that doesn't exist. 
self.assertEqual(response.status_code, 404) : The status code of what comes back, is equal to 404 because I'd expect to.


> Finally, add some testing to make sure the passengers and non-passengers lists are being generated as expected:
def test_flight_page_passengers(self):
    f = Flight.objects.get(pk=1)
    p = Passenger.objects.create(first="Alice", last="Adams")
    f.passengers.add(p)

    c = Client()
    response = c.get(f"/flights/{f.id}")
    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.context["passengers"].count(), 1)

def test_flight_page_non_passengers(self):
    f = Flight.objects.get(pk=1)
    p = Passenger.objects.create(first="Alice", last="Adams")

    c = Client()
    response = c.get(f"/flights/{f.id}")
    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.context["non_passengers"].count(), 1)
def test_flight_page_passengers(self): : Add some sample passengers to the database. 
self.assertEqual(response.context["passengers"].count(), 1) : Make sure that when you count up the number of passengers on a flight page, that that is going to be like the number 1.

@def test_flight_page_passengers: 승객 Alice 생성 - 항공편에 추가 - /flights/<id> 페이지 요청 - view에서 flight.passengers.all()에 Alice 포함 - passengers의 count == 1 확인
@def test_flight_page_non_passengers:승객 Alice 생성, 항공편에는 추가 x - /flights/<id> 페이지 요청 - view에서 Passenger.objects.exclude(flights=flight)에 Alice 포함 - non_passengers의 count == 1 확인


> 'python manage.py test' , No errors
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.048s

OK
Destroying test database for alias 'default'...

@Error: Flight.DoesNotExist: Flight matching query does not exist

- Problem: 테스트에서 존재하지 않는 flight_id로 URL 요청 - views.py에서는 Flight.objects.get(id=flight_id)를 그냥 호출해서 에러 발생
- Solution: views.py에서 from django.shortcuts import render, get_object_or_404 추가 후 flight = Flight.objects.get(id=flight_id)를 flight = get_object_or_404(Flight, id=flight_id)로 수정


Selenium

Create tests for our client-side code.

> Back to our 'counter.html' page and work on writing some tests for it. 
- Write a slightly different counter page where we include a button to decrease the count:
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Counter</title>
        <script>
            
            // Wait for page to load
            document.addEventListener('DOMContentLoaded', () => {

                // Initialize variable to 0
                let counter = 0;

                // If increase button clicked, increase counter and change inner html
                document.querySelector('#increase').onclick = () => {
                    counter ++;
                    document.querySelector('h1').innerHTML = counter;
                }

                // If decrease button clicked, decrease counter and change inner html
                document.querySelector('#decrease').onclick = () => {
                    counter --;
                    document.querySelector('h1').innerHTML = counter;
                }
            })
        </script>
    </head>
    <body>
        <h1>0</h1>
        <button id="increase">+</button>
        <button id="decrease">-</button>
    </body>
</html>
- Run the function once the DOM is loaded, once all the contents of this page have loaded the way I'd expect it to. Get the element whose ID is decrease/increase. Add an event handler in the form of the callback function that will be called when the button is clicked. Then get the h1 element and update its innerHTML and set that to whatever the value of counter happens to be. 


> To test this code, we could just open up the web browser, click the buttons and observe what happens. And it's not the type of thing where I'm simulating like a GET or a POST request. There's no server that I'm sending a request to and getting back a response. This is all happening in the browser. 
But this would become tedious for larger applications, which is why several frameworks have been created that help with in-browser testing, one of which is called Selenium
- Using Selenium, we'll be able to define a testing file in Python where we can simulate a user opening a web browser, navigating to our page, and interacting with it. 
- The main tool is a Web Driver, which will open up a web browser on the computer. 
- How we could start using this library to begin interacting with pages?
- Below, we use both 'selenium' and 'ChromeDriver'. 
- Selenium can be installed for python by running 'pip install selenium'
- 'ChromeDriver' can be installed by running 'pip install chromedriver-py'.

import os
import pathlib
import unittest

from selenium import webdriver

# Finds the Uniform Resourse Identifier of a file
def file_uri(filename):
    return pathlib.Path(os.path.abspath(filename)).as_uri()

# Sets up web driver using Google chrome
driver = webdriver.Chrome()
(I'll use Edge Driver for Microsoft Edge browser)
driver = webdriver.Edge()
- Edge/Chrome.. has the ability to allow me using automated test software, using Python code, control what it is the web browser is doing. 

> The above code is all of the basic setup, so we can get into some interesting uses by employing the Python interpreter. (cmd> python)
- One note about the first few lines is that in order to target a specific page, we need that page's Uniform Resource Identifier(URI) which is a unique string that represents that resource. 
>>> from tests import *
# Find the URI of our newly created file
>>> uri = file_uri("counter.html")

# Use the URI to open the web page
>>> driver.get(uri)

# Access the title of the current page
>>> driver.title
'Counter'

# Access the source code of the page
>>> driver.page_source
'<html lang="en"><head>\n        <title>Counter</title>\n        <script>\n            \n            // Wait for page to load\n            document.addEventListener(\'DOMContentLoaded\', () => {\n\n                // Initialize variable to 0\n                let counter = 0;\n\n                // If increase button clicked, increase counter and change inner html\n                document.querySelector(\'#increase\').onclick = () => {\n                    counter ++;\n                    document.querySelector(\'h1\').innerHTML = counter;\n                }\n\n                // If decrease button clicked, decrease counter and change inner html\n                document.querySelector(\'#decrease\').onclick = () => {\n                    counter --;\n                    document.querySelector(\'h1\').innerHTML = counter;\n                }\n            })\n        </script>\n    </head>\n    <body>\n        <h1>0</h1>\n        <button id="increase">+</button>\n        <button id="decrease">-</button>\n    \n</body></html>'

# Find and store the increase and decrease buttons:
>>> increase = driver.find_element_by_id("increase")
>>> decrease = driver.find_element_by_id("decrease")

# Simulate the user clicking on the two buttons
>>> increase.click()
>>> increase.click()
>>> decrease.click()

# We can even include clicks within other Python constructs:
>>> for i in range(25):
...     increase.click()
- Get the page's URI that will identify that page. 
- Tell this web driver, part of the Python program that is controlling the web browser that I would like to get this web page as if the user had gone to the web page and pressed Return after typing in the URL. 

- Find the element whose ID is 'increase' and save that inside of a variable. 
@ Selenium 4에서는 'find_element_by_* 계열 메서드가 삭제됨.
=> find_element(BY.ID, ...) 형태 사용

- Get the JavaScript event handler, onclick handler, and run that callback function that increases/decreases the value of counter and updates the h1. 

- Repeat increase.click() 25 times => Increase button is going to clicked 25 times very quickly. 


> How we can use this simulation to create automated tests of our page:
# Standard outline of testing class
class WebpageTests(unittest.TestCase):

    def test_title(self):
        """Make sure title is correct"""
        driver.get(file_uri("counter.html"))
        self.assertEqual(driver.title, "Counter")

    def test_increase(self):
        """Make sure header updated to 1 after 1 click of increase button"""
        driver.get(file_uri("counter.html"))
        increase = driver.find_element_by_id("increase")
        increase.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "1")

    def test_decrease(self):
        """Make sure header updated to -1 after 1 click of decrease button"""
        driver.get(file_uri("counter.html"))
        decrease = driver.find_element_by_id("decrease")
        decrease.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "-1")

    def test_multiple_increase(self):
        """Make sure header updated to 3 after 3 clicks of increase button"""
        driver.get(file_uri("counter.html"))
        increase = driver.find_element_by_id("increase")
        for i in range(3):
            increase.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "3")

if __name__ == "__main__":
    unittest.main()

import os
import pathlib
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By

def file_uri(filename):
    return pathlib.Path(os.path.abspath(filename)).as_uri()

driver = webdriver.Edge()

class WebpageTests(unittest.TestCase):
    def test_title(self):
        """Make sure title is correct"""
        driver.get(file_uri("counter.html"))
        self.assertEqual(driver.title, "Counter")

    def test_increase(self):
        """Make sure header updated to 1 after 1 click of + burron"""
        driver.get(file_uri("counter.html"))
        increase = driver.find_element(By.ID, "increase")
        increase.click()
        self.assertEqual(driver.find_element(By.TAG_NAME, "h1").text, "1")

    def test_decrease(self):
        """Make sure header updated to -1 after 1 click of - button"""
        driver.get(file_uri("counter.html"))
        decrease = driver.find_element(By.ID, "decrease")
        decrease.click()
        self.assertEqual(driver.find_element(By.TAG_NAME, "h1").text, "-1")

    def test_multiple_increase(self):
        """Make sure header update to 3 after 3 clicks of + button"""
        driver.get(file_uri("counter.html"))
        increase = driver.find_element(By.ID, "increase")
        for i in range(3):
            increase.click()
        self.assertEqual(driver.find_element(By.TAG_NAME, "h1").text, "3")

if __name__ == "__main__":
    unittest.main()

def test_title(self): : Get 'counter.html' and open up that page. Make sure the title of the page is actually 'Counter'.
def test_increase(self): : Find the element whose ID is 'increase' and click on that button to simulate a user pressing the Plus button in order to increase the value of the counter. Check that when you find element by tag name, h1, Get the h1 element and access its text property, meaning whatever contained inside of two h1 tags, I'd expect that to be the num 1. 
def test_multiple_increase(self): : Test if the button is pressed 3 times

> Run 'python tests.py'
- Our simulations will be carried out in the browser, and then the results of the tests will be printed to the console. 

- What this might look like when we have a bug in the code a test fails:
            document.querySelector('#decrease').onclick = () => {
                counter++;


- The assertion fail was on the test decrease function. It happened when I tried to assert that what was inside of the h1 element was -1, because 1 is not equal to -1.


unittest Methods

- assertEqual: I'd like to assert that two things are equal to each other
- assertNotEqual
- assertTrue
- assertIn: To check whether a string is contained in other or not. 
- assertNotIn

Django Testing

To verify that our database works the way we expected it to, and that our views works the way that we expected them to and provided the right context back to the user after the user makes a request to our web application

Browser Testing

When I want to test inside of the user's web browser. Test if browser actually works when a user clicks on the button, that the JavaScript behaves the way that I'd expect it to. 


CI/CD

CI/CD is a set of software development best practices that dictate how code is written by a team of people, and how that code is later delivered to users of the applications. 

> CI: Continuous Integration

- Frequent merges to the main branch
- Automated unit testing with each merge

> CD: Continuous Delivery

- Short release schedules, meaning new versions of an application are released frequently

> Why CI/CD is popular among SW development teams

- When different team members are working on different features, many compatibility issues can arise when multiple features are combined at the same time. => CI allows teams to tackle small conflicts as they come. 
- Unit tests are run with each Merge, when a test fails it is easier to isolate the part of the code that is causing the problem. 
- Frequently releasing new versions of an application allows developers to isolate problems if they arise after launch. 
- Releasing small, incremental changes allows users to slowly get used to new app features rather than being overwhelmed with an entirely different version.
- Not waiting to release new features allows companies to stay ahead in a competitive market. 

GitHub Actions

GitHub Actions: A popular tool used to help with CI

- Allow us to create workflows where we can specify certain actions to be performed every time someone pushes to a git repository. 
- For example, check with every push that a style guide is adhered to, or that a set of unit tests is passed. 
- Can enforce that by making sure that every time anyone pushes to a GitHub repository, we'll automatically run some GitHub action that is going to take care of the process of running tests on that program. / And via an email, GitHub might send to you to say that this particular test failed, and you'll know every time you push to that repository. 

> In order to set up a GitHub action, use a configuration language 'YAML'.
- YAML structures its data around key-value pairs(like a JSON)
Ex)
key1: value1
key2: value2
key3:
    - item1
    - item2
    - item3

> Example of how we would configure a YAML file(which takes the form 'name.yml' or 'name.yaml) that works with GitHub Actions.
- To do this, create a '.github' directory in my repository, and then a 'workflows' directory inside of that, and finally a 'ci.yml' file within that. 
name: Testing
on: push

jobs:
  test_project:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run Django unit tests
      run: |
        pip3 install --user django
        python3 manage.py test

> What each part of this file is doing
- Give the workflow a 'name
- With the 'on' key, specify when the workflow should run. / In this case, perform the tests every time someone pushes to the repository.
- The rest of the file is contained within a 'jobs' key, which indicates which jobs should be run at every push. 
-- In this case, the only job is 'test_project'. Choose any name for a job. Every job must define two components.
    1. The 'runs-on' key specifies which of GitHub's virtual machines we would like our code to be run on. / Run on the latest version of Ubuntu
    2. The 'steps' key provides the actions that should occur when this job is run.
    - In the 'uses' key we specify which GitHub action we wish to use. 'actions/checkout@v2' is an action written by GitHub that we can use. / Check out my code in the Git repository and allow me to run programs that operate on that code. 
    - The 'name' key here allows us to provide a description of the action we're taking
    - After the 'run' key, type the commands we wish to run on GitHub's server. / In this case, install Django and then run the testing file. 


> Open up our repository in GitHub and take a look at some of the tabs near the top of the page:
- Code: The tab that we've been using most frequently, as it allows us to view the files and folders within our directory.
- Issues: Here we can open and close issues, which are requests for bug fixes or new features. As a to-do list for our app
- Pull Requests: Requests from people who wish to merge some code from one branch into another one. Allows people to perform code reviews where they comment and provide suggestions before code is integrated into the master branch.
- GitHub Actions: The tab we'll use when working on CI, as it provides logs of the actions that have taken place after each push. 

> Imagine that we pushed our changes before we fixed the bug we had in the 'is_valid_flight' function in 'models.py' within our 'airport' project. Navigate to the GitHub Actions tab, click on our most recent push, click on the action that failed, and view the log
#models.py > is_valid_flight > and -> or



-> Add working directory
name: Testing
on: push

jobs:
  test_project:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run Django unit tests
        working-directory: CS50Lec7/airline0
        run: |
          pip3 install --user django
          python3 manage.py test


.....FF...
======================================================================
FAIL: test_invalid_flight_destination (flights.tests.FlightTestCase.test_invalid_flight_destination)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/runner/work/CS50-Web-Programming-with-Python-and-JavaScript/CS50-Web-Programming-with-Python-and-JavaScript/CS50Lec7/airline0/flights/tests.py", line 34, in test_invalid_flight_destination
self.assertFalse(f.is_valid_flight())
AssertionError: True is not false
======================================================================
FAIL: test_invalid_flight_duration (flights.tests.FlightTestCase.test_invalid_flight_duration)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/runner/work/CS50-Web-Programming-with-Python-and-JavaScript/CS50-Web-Programming-with-Python-and-JavaScript/CS50Lec7/airline0/flights/tests.py", line 40, in test_invalid_flight_duration
self.assertFalse(f.is_valid_flight())
AssertionError: True is not false
----------------------------------------------------------------------
Ran 10 tests in 0.039s
FAILED (failures=2)
Destroying test database for alias 'default'...
Found 10 test(s).
System check identified no issues (0 silenced).
Error: Process completed with exit code 1.


> Now, after fixing the bug, we could bush again and find a better outcome
#models.py > is_valid_flight > or -> and

Ran 10 tests in 0.040s
OK
Destroying test database for alias 'default'...
Found 10 test(s).
System check identified no issues (0 silenced).


Docker

> Problems can arise in the sw development when the configuration on the computer is different than the one your application is being run on. 
> To avoid these problems, we need a way to make sure everyone working on a project is using the same environment. 
- Use a tool 'Docker' which is a containerization software, meaning it creates an isolated environment within your computer that can be standardized among many collaborators and the server on which your site is run. 
- Docker is a bit like a Virtual Machine.
- A virtual machine is an entire virtual computer with its own operating system, meaning it ends up taking a lot of space wherever it is running. 
- Dockers work by setting up a container within an existing computer, therefore taking up less space. 


> How to configure a Docker container on a computer
- Create a Docker File 'Dockerfile'. Inside this, we'll provide instructions for how to create a Docker Image which describes the libraries and binaries we wish to include in our container. 
FROM python:3
COPY .  /usr/src/app
WORKDIR /usr/src/app
RUN pip install -r requirements.txt
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
- This Docker file describes how I might create a container that is going to run my Django web application. 

> What the above file actually does:
FROM python:3: This shows that we are basing this image off or a standard image in which Python 3 is installed. This is fairly common when writing a Docker File, as it allows you to avoid the work of re-defining the same basic setup with each new image. 
COPY .  /usr/src/app: This shows that we wish to copy everything from our current directory (.) and store it in the '/usr/src/app' directory in our new container. 
WORKDIR /usr/src/app: This sets up where we will run commands within the container (like 'cd' on the terminal) / I'd like to set my working directory equal to that same application directory. The application directory inside of the container that now contains all of the files from my application because I copied all of those files into the container. 
RUN pip install -r requirements.txt: In this line, assuming you've included all of your requirements to a file called 'requirements.txt, they will all be installed within the container.
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]: Finally, we specify the command that should be run when we start up the container. 

> We've been using SQLite as that's the default database management system for Django. 
> In live applications with real users though, SQLite is almost never used, as it is not as easily scaled as other systems. 
> In most real web applications that are working with many users, we want our database hosted elsewhere on some separate server to be able to handle its own incoming requests and connections. 
- Instead of using SQLite, I'd like to use Postgres, for example, as the database server that I'd like to run. 
- In addition to running my web application in one server, I also need another server that's running Postgres, such that I can communicate with that Postgres database instead. 
- To run a separate server for our database, add another Docker container and run them together using a feature called Docker Compose
- This will allow two different servers to run in separate containers, but also be able to communicate with one another. 
- To specify this, use a YAML file called 'docker-compose.yml'
version: '3'

services:
    db:
        image: postgres

    web:
        build: .
        volumes:
            - .:/usr/src/app
        ports:
            - "8000:8000"

> In the above file,
- Specify that we're using version 3 of Docker Compose
- Outline two services:
    1. 'db' sets up our database container based on an image already written by Postgres
    2. 'web' sets up our server's container by instructing Docker to
    - Use the Dockerfile within the current directory
    - Use the specified path within the container
    - Link port 8000 within the container to port 8000 on our computer

> 'docker compose up': This will launch both of our servers inside of new Docker containers.
> At this point, we want to run commands within our Docker container to add database entries or run tests. 
- To do this, run 'docker ps' to show all of the docker containers that are running. 
- Then, we'll find the 'CONTAINER ID' of the container we wish to enter and run 'docker exec -it CONTAINER_ID bash -1'. => This will move you inside the 'usr/src/app' directory we set up within our container. / 'python manage.py createsuperuser' to create a superuser. 
- We can run any commands('python mange.py migrate')we wish inside that container and then exit by running 'CTRL-D'

> Now I can begin to manipulate this database which is a Postgres database running in a separate container.



댓글 없음:

댓글 쓰기

Bottom Ad [Post Page]