956 lines
30 KiB
Markdown
956 lines
30 KiB
Markdown
# List of contents
|
||
1. [[#REST API with Spring]]
|
||
2. [[#Flask API in Python]]
|
||
3. [[#gRPC]]
|
||
4. [[#JUnit]]
|
||
5. [[#EasyMock]]
|
||
6. [[#DOCKER]]
|
||
|
||
<div class="page-break"></div>
|
||
|
||
# REST API in Java with Spring
|
||
## Making REST API Requests
|
||
|
||
### With ***WebClient*** object:
|
||
(e.g Spring WebFlux)
|
||
#### GET
|
||
```java
|
||
public void getAllPersons(CustomerSortingOptions sortingOptions, Consumer<List<Customer>> customerConsumer) {
|
||
webClient.get()
|
||
.uri(uriBuilder -> uriBuilder
|
||
.path("customers")
|
||
.queryParam("sortField", sortingOptions.getSortField())
|
||
.queryParam("sortingOrder", sortingOptions.getSortingOrder())
|
||
.build())
|
||
.retrieve()
|
||
.bodyToMono(new ParameterizedTypeReference<List<Customer>>() {
|
||
})
|
||
.onErrorStop()
|
||
.subscribe(newPersons -> {
|
||
customers.clear();
|
||
customers.addAll(newPersons);
|
||
customerConsumer.accept(customers);
|
||
});
|
||
}
|
||
|
||
```
|
||
|
||
#### POST
|
||
```java
|
||
public void addPerson(Customer customer, Consumer<List<Customer>> customerConsumer) {
|
||
// TODO Part 2: Complete the http post request to the server
|
||
webClient.post()
|
||
.uri("customers")
|
||
.bodyValue(customer)
|
||
.retrieve()
|
||
.bodyToMono(Customer.class)
|
||
.onErrorStop()
|
||
.subscribe(newCustomer -> {
|
||
customers.add(newCustomer);
|
||
customerConsumer.accept(customers);
|
||
});
|
||
}
|
||
```
|
||
|
||
#### PUT
|
||
```java
|
||
public void updatePerson(Customer customer, Consumer<List<Customer>> customerConsumer) {
|
||
// TODO Part 2: Complete the http put request to the server
|
||
webClient.put()
|
||
.uri("customers/" + customer.getId())
|
||
.bodyValue(customer)
|
||
.retrieve()
|
||
.bodyToMono(Customer.class)
|
||
.onErrorStop()
|
||
.subscribe(newCustomer -> {
|
||
customers.replaceAll(x -> x.getId().equals(customer.getId()) ? newCustomer : x);
|
||
customerConsumer.accept(customers);
|
||
});
|
||
}
|
||
```
|
||
|
||
#### DELETE
|
||
```java
|
||
public void deletePerson(Customer customer, Consumer<List<Customer>> customerConsumer) {
|
||
// TODO Part 2: Make an http delete request to the server
|
||
webClient.delete()
|
||
.uri("customers/" + customer.getId())
|
||
.retrieve()
|
||
.toBodilessEntity()
|
||
.onErrorStop()
|
||
.subscribe(deletedCustomer -> {
|
||
customers.remove(customer);
|
||
customerConsumer.accept(customers);
|
||
});
|
||
}
|
||
```
|
||
|
||
#### Just in case: webClient Constructor
|
||
```java
|
||
public CustomerController() {
|
||
this.webClient = WebClient.builder()
|
||
.baseUrl("http://localhost:8080/")
|
||
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
|
||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||
.build();
|
||
this.customers = new ArrayList<>();
|
||
}
|
||
```
|
||
|
||
#### With Request Parameters:
|
||
Endpoint:
|
||
```java
|
||
@GetMapping("persons")
|
||
public ResponseEntity<List<Person>> getAllPersons(PersonSortingOptions sortingOptions) { // Don't use @RequestParams unless object is simple(for example String), for more complex objects Spring parses the object by itself
|
||
List<Person> persons = personService.getAllPersons(sortingOptions);
|
||
return ResponseEntity.ok(persons);
|
||
}
|
||
```
|
||
Request:
|
||
```java
|
||
public void getAllPersons(PersonSortingOptions sortingOptions, Consumer<List<Person>> personsConsumer) {
|
||
this.webClient.get()
|
||
.uri(uriBuilder -> uriBuilder // Use uriBuilder to build the path
|
||
.path("persons")
|
||
.queryParam("sortingOrder",sortingOptions.getSortingOrder()) // The fields are passed separately
|
||
.queryParam("sortField",sortingOptions.getSortField()) // In the format: ("Name",value)
|
||
.build()
|
||
)
|
||
.retrieve()
|
||
.bodyToMono(new ParameterizedTypeReference<List<Person>>() {
|
||
})
|
||
.onErrorStop()
|
||
.subscribe(persons -> {
|
||
this.persons = persons;
|
||
personsConsumer.accept(this.persons);
|
||
});
|
||
|
||
}
|
||
```
|
||
|
||
### With *RestTemplate*:
|
||
```java
|
||
GetMapping("/getTimeLine/{id}")
|
||
public List<Tweet> getTimeLine(@PathVariable("id") int userID) {
|
||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||
|
||
ResponseEntity<List<Tweet>> response = restTemplate.exchange(
|
||
getActivityURL + userID, // Set the uri HttpMethod.GET,
|
||
null, // requestEntity = null as there is no body,
|
||
new ParameterizedTypeReference<List<Tweet>>(){} // response body type );
|
||
return response.getBody();
|
||
}
|
||
```
|
||
## REST API Endpoints
|
||
|
||
Example Java class:
|
||
```java
|
||
@RestController
|
||
@RequestMapping("/activity")
|
||
public class ActivityController {
|
||
private Map<Integer, List<Tweet>> userActivityMap = new HashMap<>();
|
||
private Map<Integer, List<User>> userFollowedMap = new HashMap<>();
|
||
|
||
@GetMapping("/getActivity/{id}")
|
||
public List<Tweet> getActivity(@PathVariable("id") int userID) {
|
||
List<Tweet> userTweets = userActivityMap.get(userID);
|
||
|
||
if(userTweets == null) return new ArrayList<>();
|
||
|
||
return userTweets;
|
||
}
|
||
|
||
@PostMapping("/addActivity")
|
||
public void addActivity(@RequestBody Tweet tweet) {
|
||
userActivityMap.computeIfAbsent(tweet.getUser().getUserID(), x -> new LinkedList<>());
|
||
userActivityMap.get(tweet.getUser().getUserID()).add(tweet);
|
||
}
|
||
|
||
@PostMapping("/addFollower")
|
||
public void addFollower(@RequestBody FollowRequest followRequest) {
|
||
User follower = followRequest.getFollower();
|
||
User followed = followRequest.getFollowed();
|
||
|
||
userFollowedMap.computeIfAbsent(follower.getUserID(),x -> new LinkedList<>())
|
||
.add(followed);
|
||
}
|
||
|
||
@DeleteMapping("/deleteActivity")
|
||
public void deleteActivity(@RequestBody Tweet tweet) {
|
||
List<Tweet> userTweets = userActivityMap.get(tweet.getUser().getUserID());
|
||
if(userTweets == null) return;
|
||
|
||
userTweets.remove(tweet);
|
||
}
|
||
|
||
@DeleteMapping("/deleteFollower")
|
||
public void deleteFollower(@RequestBody FollowRequest followRequest) {
|
||
User follower = followRequest.getFollower();
|
||
User followed = followRequest.getFollowed();
|
||
|
||
List<User> followedUsers = userFollowedMap.get(follower.getUserID());
|
||
if(followedUsers == null) return;
|
||
|
||
followedUsers.remove(followed);
|
||
}
|
||
|
||
@GetMapping("/getFollowedList/{id}")
|
||
public List<User> getFollowedList(@PathVariable("id") int userID) {
|
||
// TODO: this method is also not complete, so you have to implement it accordingly :)
|
||
List<User> followed = userFollowedMap.get(userID);
|
||
if(followed == null) return new ArrayList<>();
|
||
return followed;
|
||
}
|
||
}
|
||
```
|
||
|
||
## Excersises:
|
||
- L02PE01 TUM Social App
|
||
- L02PE02 REST Architectural Style Part 2
|
||
- L09PE02 Wizardry Character Creation
|
||
-
|
||
|
||
<div class="page-break"></div>
|
||
# Flask API in Python
|
||
## *Server side* endpoints
|
||
|
||
#### GET
|
||
```python
|
||
@api.route("/<key>", methods=["GET"])
|
||
def get(key: str) -> Response:
|
||
"""
|
||
Retrieve the file-content associated with the given key owned by the user.
|
||
If there is no user-id `uid` given in the request body or the user-id does not match, return error message and status.
|
||
Otherwise, return the file-content and success status.
|
||
"""
|
||
request_body: dict = request.json
|
||
ff: FileFolder = current_app.ff # type: ignore
|
||
|
||
user_id = request_body.get("uid")
|
||
|
||
if user_id is None:
|
||
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
|
||
|
||
file = ff.get(key)
|
||
|
||
if file is None:
|
||
return Response(response=KEY_DOES_NOT_EXIST % key, status=NOT_FOUND)
|
||
|
||
if file.uid != user_id:
|
||
return Response(response=NO_ACCESS_TO_ENTRY % key, status=BAD_REQUEST)
|
||
|
||
return Response(response=file.content,status=OK)
|
||
pass
|
||
```
|
||
#### PUT
|
||
```python
|
||
@api.route("/<key>", methods=["PUT"])
|
||
def put(key: str) -> Response:
|
||
"""
|
||
Upsert a file-content associated with the given key owned by the user.
|
||
If there is no user-id `uid` or file-content `content` given in the request body, return error message and status.
|
||
Otherwise, return the success message and success status
|
||
"""
|
||
request_body: dict = request.json
|
||
ff: FileFolder = current_app.ff
|
||
|
||
if "uid" not in request_body:
|
||
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
|
||
if "content" not in request_body:
|
||
return Response(NO_CONTENT_IN_REQUEST_BODY,BAD_REQUEST)
|
||
|
||
uid = request_body["uid"]
|
||
file_content = request_body["content"]
|
||
|
||
old_entry = ff.get(key)
|
||
|
||
if old_entry is None:
|
||
entry = Entry(uid,file_content)
|
||
success = ff.put(key,entry)
|
||
if success:
|
||
return Response(PUT_SUCCESS,OK)
|
||
else:
|
||
return Response(NO_SPACE_IN_FILE_FOLDER,BAD_REQUEST)
|
||
|
||
|
||
if uid != old_entry.uid:
|
||
return Response(NO_ACCESS_TO_ENTRY % key,status=BAD_REQUEST)
|
||
|
||
entry = Entry(uid,file_content)
|
||
ff.put(key,entry)
|
||
|
||
return Response(PUT_SUCCESS,OK)
|
||
pass
|
||
```
|
||
#### DELETE
|
||
```python
|
||
@api.route("/<key>", methods=["DELETE"])
|
||
def remove(key: str) -> Response:
|
||
"""
|
||
Remove the file-content associated with the given key owned by the user.
|
||
If there is no user-id `uid` given in the request body or the user-id does not match, return error message and status.
|
||
Otherwise, return the removed file-content and success status
|
||
"""
|
||
request_body: dict = request.json
|
||
ff: FileFolder = current_app.ff
|
||
|
||
if "uid" not in request_body:
|
||
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
|
||
|
||
uid = request_body["uid"]
|
||
|
||
entry = ff.get(key)
|
||
|
||
if entry is None:
|
||
return Response(KEY_DOES_NOT_EXIST % key,NOT_FOUND)
|
||
if entry.uid != uid:
|
||
return Response(NO_ACCESS_TO_ENTRY % key,BAD_REQUEST)
|
||
|
||
ff.remove(key)
|
||
|
||
return Response(entry.content,OK)
|
||
pass
|
||
```
|
||
#### GET - all
|
||
```python
|
||
@api.route("/", methods=["GET"])
|
||
def list() -> Response:
|
||
"""
|
||
List all (key, file-content) tuples owned by the user.
|
||
If there is no user-id `uid` given in the request body, return error message and status.
|
||
Otherwise, return the list of file-content and success status.
|
||
"""
|
||
request_body: dict = request.json
|
||
ff: FileFolder = current_app.ff
|
||
|
||
if "uid" not in request_body:
|
||
return Response(NO_UID_IN_REQUEST_BODY,BAD_REQUEST)
|
||
|
||
uid = request_body["uid"]
|
||
|
||
items = ff.items()
|
||
|
||
output = []
|
||
|
||
for key, entry in items:
|
||
output.append(f"{key}:{entry.content}")
|
||
output_string = ".".join(output)
|
||
|
||
return Response(output_string,OK)
|
||
pass
|
||
```
|
||
|
||
## Making requests on the *client side*
|
||
|
||
#### Helper method
|
||
```python
|
||
# make json request to server
|
||
def json_request(self, method: str, path: str, body: dict):
|
||
self.conn.request(method, path, json.dumps(body), headers={"Content-Type": "application/json"})
|
||
flask_response = self.conn.getresponse()
|
||
return flask_response.read().decode()
|
||
```
|
||
|
||
#### PUT
|
||
```python
|
||
def put(self, key, content):
|
||
"""
|
||
Upsert (key, content) tuple into the file folder.
|
||
Return the server response
|
||
"""
|
||
request_body = {"uid": self.user_id, "content": content}
|
||
return self.json_request("PUT", f"/{key}",request_body)
|
||
pass
|
||
```
|
||
#### GET
|
||
```python
|
||
def read(self, key):
|
||
"""
|
||
Retrieve the file-content associated with the given key owned by the user.
|
||
Return the server response
|
||
"""
|
||
request_body = {"uid": self.user_id}
|
||
return self.json_request("GET",f"/{key}",request_body)
|
||
pass
|
||
```
|
||
#### DELETE
|
||
```python
|
||
def remove(self, key):
|
||
"""
|
||
Remove the file-content associated with the given key owned by the user.
|
||
Return the server response
|
||
"""
|
||
|
||
request_body = {"uid": self.user_id}
|
||
return self.json_request("DELETE",f"/{key}",request_body)
|
||
pass
|
||
```
|
||
#### GET - ALL
|
||
```python
|
||
def list(self):
|
||
"""
|
||
List all file-content owned by the user.
|
||
Return the server response
|
||
"""
|
||
|
||
request_body = {"uid": self.user_id}
|
||
return self.json_request("GET","/",request_body)
|
||
pass
|
||
```
|
||
## Excersises:
|
||
- L03PE01 Online FileFolder
|
||
|
||
# Python - general syntax and tips:
|
||
## Dictonaries:
|
||
```python
|
||
thisdict = {
|
||
"brand": "Ford",
|
||
"model": "Mustang",
|
||
"year": 1964
|
||
}
|
||
print(thisdict["brand"])
|
||
```
|
||
|
||
```python
|
||
thisdict = dict(name = "John", age = 36, country = "Norway")
|
||
print(thisdict)
|
||
```
|
||
You can check if a key exists with the keyword *in*:
|
||
```python
|
||
if "model" in thisdict:
|
||
print("Yes, 'model' is one of the keys in the thisdict dictionary")
|
||
```
|
||
|
||
You can loop over keys and values in dictionary:
|
||
```python
|
||
for x in thisdict: # Loop over keys
|
||
print(x)
|
||
for x in thisdict.keys(): #Another way to loop over keys
|
||
print(x)
|
||
|
||
for x in thisdict: # Loop over values
|
||
print(thisdict[x])
|
||
for x in thisdict.values(): # Another way to loop over values
|
||
print(x)
|
||
|
||
for x, y in thisdict.items(): # Looping over key-value pairs as tuples
|
||
print(x, y)
|
||
```
|
||
|
||
### Dictionary methods
|
||
| Method | Description |
|
||
| ------------ | ----------------------------------------------------------------------------------------------------------- |
|
||
| clear() | Removes all the elements from the dictionary |
|
||
| copy() | Returns a copy of the dictionary |
|
||
| fromkeys() | Returns a dictionary with the specified keys and value |
|
||
| get() | Returns the value of the specified key |
|
||
| items() | Returns a list containing a tuple for each key value pair |
|
||
| keys() | Returns a list containing the dictionary's keys |
|
||
| pop() | Removes the element with the specified key |
|
||
| popitem() | Removes the last inserted key-value pair |
|
||
| setdefault() | Returns the value of the specified key. If the key does not exist: insert the key, with the specified value |
|
||
| update() | Updates the dictionary with the specified key-value pairs |
|
||
| values() | Returns a list of all the values in the dictionary |
|
||
|
||
<div class="page-break"></div>
|
||
|
||
# gRPC
|
||
|
||
**gRPC reference: https://grpc.io/docs/**
|
||
**gRPC Java basics: https://grpc.io/docs/languages/java/basics/**
|
||
|
||
## Protobuf files
|
||
#### Defining classes:
|
||
|
||
Important tips:
|
||
* Array in protobuf means ```repeated``` field
|
||
|
||
```protobuf
|
||
message Date {
|
||
int32 day = 1; // Number specifies order
|
||
int32 month = 2;
|
||
int32 year = 3;
|
||
}
|
||
|
||
message Location {
|
||
string city = 1;
|
||
string country = 2;
|
||
}
|
||
|
||
message LocationDate {
|
||
Location location = 1;
|
||
Date date = 2;
|
||
}
|
||
```
|
||
|
||
#### Defining services:
|
||
```protobuf
|
||
service WeatherReporter {
|
||
|
||
//Returning one object
|
||
rpc GetCityWeatherSingleDay(LocationDate) returns (CityWeatherData) {}
|
||
|
||
// Returning a stream of objects
|
||
rpc GetCityWeatherMultipleDays(LocationDatePeriod) returns (stream CityWeatherData) {}
|
||
}
|
||
```
|
||
|
||
#### Example of services and objects with **repeated** fields
|
||
This will be utilized by some of the client side examples underneath
|
||
```protobuf
|
||
service MetadataService {
|
||
rpc getTrackMetadata(EntitiesRequest) returns (Tracks) {}
|
||
|
||
rpc getAlbumMetadata(EntitiesRequest) returns (Albums) {}
|
||
}
|
||
|
||
message EntitiesRequest {
|
||
repeated uint32 id = 1;
|
||
}
|
||
|
||
message Track {
|
||
uint64 id = 1;
|
||
string name = 2;
|
||
string album = 3;
|
||
string artist = 4;
|
||
uint32 duration = 5;
|
||
}
|
||
|
||
message Tracks {
|
||
repeated Track track = 1;
|
||
}
|
||
|
||
message Album {
|
||
uint64 id = 1;
|
||
string name = 2;
|
||
string artist = 3;
|
||
repeated uint32 tracks = 4;
|
||
}
|
||
|
||
message Albums {
|
||
repeated Album album = 1;
|
||
}
|
||
|
||
```
|
||
#### Generating classes from *.proto* files
|
||
1. Gradle tab in IDE and press "Tasks > other > generateProto")
|
||
2. The generated classes from the proto file should be in build/generated/source/proto/main folder
|
||
|
||
## Server side
|
||
|
||
In practice can be done with streams or with simple iteration
|
||
```java
|
||
public void getCityWeatherSingleDay(LocationDate request, StreamObserver<CityWeatherData> responseObserver) {
|
||
var data = allWeatherData.stream().filter(cityWeatherData -> cityWeatherData.getLocationDate().equals(request)).findFirst().orElse(null);
|
||
if(data != null)responseObserver.onNext(data);
|
||
responseObserver.onCompleted();
|
||
}
|
||
```
|
||
|
||
#### For returning a *stream* of data just call onNext multiple times
|
||
With iteration:
|
||
```Java
|
||
public void getCityWeatherMultipleDays(LocationDatePeriod request, StreamObserver<CityWeatherData> responseObserver) {
|
||
for(CityWeatherData data : allWeatherData) {
|
||
if(data.getLocationDate().getLocation().equals(request.getLocation())) {
|
||
if(compareDate(data.getLocationDate().getDate(), request.getStartDate()) >= 0 && compareDate(data.getLocationDate().getDate(), request.getEndDate()) <= 0)
|
||
{
|
||
responseObserver.onNext(data);
|
||
}
|
||
}
|
||
}
|
||
responseObserver.onCompleted();
|
||
}
|
||
```
|
||
|
||
With streams:
|
||
```java
|
||
public void getCityWeatherMultipleDays(LocationDatePeriod request, StreamObserver<CityWeatherData> responseObserver) {
|
||
var startDate = request.getStartDate();
|
||
var endDate = request.getEndDate();
|
||
allWeatherData.stream()
|
||
.filter(cityWeatherData -> cityWeatherData.getLocationDate().getLocation().equals(request.getLocation()))
|
||
.filter(cityWeatherData -> compareDate(cityWeatherData.getLocationDate().getDate(),startDate) >= 0)
|
||
.filter(cityWeatherData -> compareDate(cityWeatherData.getLocationDate().getDate(),endDate) <= 0)
|
||
.forEach(responseObserver::onNext);
|
||
responseObserver.onCompleted();
|
||
}
|
||
```
|
||
|
||
#### For returning an object which contains array of other objects (stupid???)
|
||
```java
|
||
public void getAlbumMetadata(EntitiesRequest request, StreamObserver<Albums> responseObserver) {
|
||
Albums.Builder albumsBuilder = Albums.newBuilder();
|
||
|
||
for(int i = 0; i < request.getIdCount();i++) {
|
||
// Find the album by its id
|
||
Album album = database.findAlbumById(request.getId(i));
|
||
|
||
if(album != null) {
|
||
albumsBuilder.addAlbum(album);
|
||
}
|
||
}
|
||
|
||
// In case that an album isn't found builder builds empty list
|
||
Albums albums = albumsBuilder.build();
|
||
responseObserver.onNext(albums);
|
||
responseObserver.onCompleted();
|
||
}
|
||
```
|
||
|
||
#### If you want to return an error
|
||
```java
|
||
public void getTrafficData(RouteRequest request, StreamObserver<TrafficResponse> responseObserver) {
|
||
var trafficModel = db.getTrafficModel(request);
|
||
var response = trafficModel.query(request);
|
||
if(response.isPresent()) {
|
||
responseObserver.onNext(response.get());
|
||
responseObserver.onCompleted();
|
||
} else {
|
||
// Just pass an exception with the onError method
|
||
responseObserver.onError(new Exception("No data available."));
|
||
}
|
||
}
|
||
```
|
||
## Client Side
|
||
|
||
#### Setup
|
||
|
||
The **blocking stub** is a placeholder on which functions are called. It needs to be received from the gRPC object in some way.
|
||
|
||
```java
|
||
public WeatherClient(Channel channel) {
|
||
// Get blocking stub from the gRPC object
|
||
blockingStub = WeatherReporterGrpc.newBlockingStub(channel);
|
||
}
|
||
```
|
||
or
|
||
```java
|
||
public SpotifyClient(Channel channel) {
|
||
// spotifyConnection is a wrapper defined in the excercise
|
||
// this is just an example that there can be other ways to get the stub
|
||
this.spotifyConnection = new SpotifyConnection(channel);
|
||
this.blockingStub = spotifyConnection.getStub();
|
||
}
|
||
```
|
||
|
||
#### Calling method with ***single object*** return value
|
||
```java
|
||
public void getCityWeatherSingleDay(int day, int month, int year, String city, String country) {
|
||
// Need to use builders to create the objects for the request
|
||
Date date = Date.newBuilder().setDay(day).setMonth(month).setYear(year).build();
|
||
Location location = Location.newBuilder().setCity(city).setCountry(country).build();
|
||
|
||
// Building the locationDate which will be passed as a parameter
|
||
LocationDate locationDate = LocationDate.newBuilder().setDate(date).setLocation(location).build();
|
||
|
||
// Getting the returned object from calling the method on the stub
|
||
CityWeatherData cityWeatherData = blockingStub.getCityWeatherSingleDay(locationDate);
|
||
|
||
System.out.println(cityWeatherData.toString());
|
||
}
|
||
```
|
||
|
||
#### Calling method which returns ***stream***
|
||
|
||
```java
|
||
public void getCityWeatherMultipleDays(int startDay, int startMonth, int startYear, int endDay, int endMonth, int endYear, String city, String country) {
|
||
// Building objects for the request
|
||
Date startDate = Date.newBuilder()
|
||
.setDay(startDay)
|
||
.setMonth(startMonth)
|
||
.setYear(startYear)
|
||
.build();
|
||
Date endDate = Date.newBuilder()
|
||
.setDay(endDay)
|
||
.setMonth(endMonth)
|
||
.setYear(endYear)
|
||
.build();
|
||
Location location = Location.newBuilder().setCity(city).setCountry(country).build();
|
||
|
||
// Building LocationDatePeriod object to be passed as parameter
|
||
LocationDatePeriod locationDatePeriod = LocationDatePeriod.newBuilder()
|
||
.setStartDate(startDate)
|
||
.setEndDate(endDate)
|
||
.setLocation(location)
|
||
.build();
|
||
|
||
// Method call returns Iterator<CityWeatherData> for the stream of CityWeatherData
|
||
Iterator<CityWeatherData> iterator = blockingStub.getCityWeatherMultipleDays(locationDatePeriod);
|
||
|
||
// Iterating over the objects
|
||
while (iterator.hasNext()) {
|
||
CityWeatherData cityWeatherData = iterator.next();
|
||
System.out.println(cityWeatherData.toString());
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Calling method which has *parameter with **repeated** field*
|
||
|
||
```java
|
||
public String displayPlaylist(int[] playlist) throws SpotifyClientException {
|
||
StringBuilder stringBuilder = new StringBuilder();
|
||
|
||
EntitiesRequest.Builder builder = EntitiesRequest.newBuilder();
|
||
|
||
// IMPORTANT for REPEATED fields
|
||
// Add all id's to request
|
||
for(int id : playlist) {
|
||
builder.addId(id);
|
||
}
|
||
|
||
EntitiesRequest request = builder.build();
|
||
|
||
// Unlike in earlier examplese returns OBJECT which contains multiple Tracks
|
||
Tracks tracks;
|
||
|
||
tracks = blockingStub.getTrackMetadata(request);
|
||
if(tracks.getTrackCount() != playlist.length) throw new SpotifyClientException("Failed to get track metadata");
|
||
|
||
|
||
for(int i = 0; i < tracks.getTrackCount();i++) {
|
||
Track track = tracks.getTrack(i);
|
||
|
||
int durationInSeconds = track.getDuration();
|
||
int durationInMinutes = durationInSeconds / 60;
|
||
durationInSeconds = durationInSeconds % 60;
|
||
|
||
stringBuilder.append(String.format("%d. %s - %s (%d:%02d)%n", i+1,track.getName(), track.getArtist(), durationInMinutes, durationInSeconds));
|
||
}
|
||
|
||
return stringBuilder.toString();
|
||
}
|
||
```
|
||
|
||
## Excersises:
|
||
- L02PB01 Weather Reporter
|
||
- L02P01 Spotify gRPC
|
||
- L03PE03 Gobblemaps
|
||
# JUnit
|
||
|
||
## assertThrows()
|
||
```java
|
||
@Test
|
||
public void testSetOnlineCourseUrlWithInvalidUrl() {
|
||
OnlineCourse course = new OnlineCourse("test");
|
||
// You need to pass the CLASS of the exception and a runnable which calls the function
|
||
// then the test will pass if EXACTLY this exception is throwed
|
||
// If you want to test for any exception pass Exception.class however, that is usually discouraged and not needed
|
||
assertThrows(MalformedURLException.class,() -> course.setUrl("hjdahjsd"));
|
||
}
|
||
```
|
||
## assertDoesNotThrow()
|
||
```java
|
||
@Test
|
||
public void testSetOnlineCourseUrlWithValidUrl(){
|
||
OnlineCourse course = new OnlineCourse("test");
|
||
// Simply assertDoesNotThrow with a runnable
|
||
assertDoesNotThrow(() -> course.setUrl("https://test"));
|
||
|
||
// We need to handle constructor of URL throwing an exception despite knowing that it won't as we are passing it a correct URL
|
||
try {
|
||
assertEquals(new URL("https://test"),course.getUrl());
|
||
} catch (MalformedURLException e) {
|
||
}
|
||
}
|
||
```
|
||
# EasyMock
|
||
|
||
`@TestSubject` annotations automatically calls the constructor with the mockObjects. However it is not applicable for constructors with other parameters that are not mocked(for example primitive types). In such cases you can call the constructor of the **test subject** as usual inside the test or in a @Before method or wherever you want.
|
||
|
||
**Second example contains how to expect *void* method calls**
|
||
## Example with TestSubject
|
||
|
||
```java
|
||
// Needs to extend the tests with EasyMock
|
||
@ExtendWith(EasyMockExtension.class)
|
||
class DiscussionTest {
|
||
|
||
// TestSubject annotations automatically calls the constructor with the mockObjects
|
||
// However it is not applicable for constructors with other parameters
|
||
|
||
@TestSubject
|
||
private Discussion discussion;
|
||
|
||
|
||
// Mock objects need to be annotated with @Mock
|
||
@Mock
|
||
private Course courseMock;
|
||
|
||
@Mock
|
||
private Comment commentMock;
|
||
|
||
// TODO implement the tests
|
||
|
||
@Test
|
||
public void testComment() {
|
||
// First we need to record
|
||
// Expect a function call and set a predetermined return value
|
||
// This expects the method to be called exactly once
|
||
expect(commentMock.save()).andReturn(true);
|
||
|
||
// After setting all expectations we need to replay the mock - e.g set it so it behaves the way we recorded
|
||
replay(commentMock);
|
||
|
||
// Test your objects as usual
|
||
discussion = new Discussion();
|
||
assertEquals(0,discussion.getNumberOfComments());
|
||
assertTrue(discussion.addComment(commentMock));
|
||
assertEquals(1,discussion.getNumberOfComments());
|
||
|
||
// At the end you need to verify the mock - this checks whether the expected methods were actually called
|
||
verify(commentMock);
|
||
}
|
||
|
||
// The methods below show the same usage
|
||
@Test
|
||
public void testCommentIfSavingFails() {
|
||
expect(commentMock.save()).andReturn(false);
|
||
replay(commentMock);
|
||
|
||
discussion = new Discussion();
|
||
assertEquals(0,discussion.getNumberOfComments());
|
||
assertFalse(discussion.addComment(commentMock));
|
||
assertEquals(0,discussion.getNumberOfComments());
|
||
|
||
verify(commentMock);
|
||
}
|
||
|
||
@Test
|
||
public void testStartCourseDiscussion() {
|
||
Person person = new Lecturer("","",LocalDate.now());
|
||
String testTopic = "testTopic";
|
||
|
||
expect(courseMock.isDiscussionAllowed(person)).andReturn(true);
|
||
|
||
replay(courseMock);
|
||
assertTrue(discussion.startCourseDiscussion(courseMock,person,testTopic));
|
||
assertEquals(courseMock,discussion.getCourse());
|
||
assertEquals(testTopic,discussion.getTopic());
|
||
|
||
verify(courseMock);
|
||
}
|
||
```
|
||
|
||
|
||
## Example with custom Constructor (also contains expecting *void* call)
|
||
|
||
```java
|
||
@Mock
|
||
Platform platformMock;
|
||
|
||
...
|
||
|
||
@Test
|
||
public void addNoteGetsURIFromPlatform() throws URISyntaxException {
|
||
URI uri = URI.create("https://notes.tum.de/examplePath"); // An example uri your mock can return
|
||
String expectedWriteDataPath = uri.getPath(); // The path that the NotesServer should write to
|
||
|
||
// Set the mock to expect the call and return the set URI
|
||
expect(platformMock.nextURI()).andReturn(uri);
|
||
|
||
// To expect a void method call you can just call the method like usual before **replaying** the mock
|
||
platformMock.writeData(expectedWriteDataPath,"Some text");
|
||
|
||
// Replay the mock
|
||
replay(platformMock);
|
||
|
||
// Instead of annotating with @TestSubject we instantiate the testServer(System Under Test) inside the Test
|
||
NotesServer testServer = new NotesServer(platformMock, false, false);
|
||
|
||
// Test like usual
|
||
URI testUri = testServer.addNote("alice", "Some text");
|
||
assertEquals(expectedWriteDataPath,testUri.getPath());
|
||
|
||
// At the end you need to verify the mock - this checks whether the expected methods were actually called
|
||
verify(platformMock);
|
||
}
|
||
```
|
||
|
||
## If you want multiple method invocations
|
||
|
||
|
||
| Method | Behaviour |
|
||
| ---------------- | -------------------------------------------------------------------------------------- |
|
||
| `.times(3)` | Expects the method to be called **exactly** 3 times. |
|
||
| `.times(2,5)` | Expects the method to be called between **2 and 5** times (inclusive). |
|
||
| `.atLeastOnce()` | Expects the method to be called **1 or more** times. |
|
||
| `.anyTimes()` | The method can be called **0 or more** times (it will never cause `verify()` to fail). |
|
||
For *void* methods:
|
||
|
||
```java
|
||
commentMock.update(); // Call the void method
|
||
expectLastCall().times(3); // Set the invocation count
|
||
```
|
||
<div class="page-break"></div>
|
||
|
||
## ALL RESOURCE LINKS:
|
||
- **Lecture slides (PDF)**
|
||
- **Your own homework solutions (cloned)**
|
||
- **Cheat-sheets/self-written notes (printed/on paper/PDF)**
|
||
- **C reference:** https://en.cppreference.com/w/c/language
|
||
- **Python reference:** https://docs.python.org/3/reference/index.html
|
||
- **Java reference:** https://www.baeldung.com/get-started-with-java-series
|
||
- **Java reference:** https://docs.oracle.com/en/java/javase/22/
|
||
- **gRPC reference:** https://grpc.io/docs/
|
||
- **Spotbugs**: https://spotbugs.readthedocs.io
|
||
- **AddressSanitizer**: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
|
||
- **Spring Boot:** https://www.baeldung.com/spring-boot
|
||
- **Spring Boot:** https://spring.io/projects/spring-boot
|
||
- **Docker:** https://docs.docker.com/
|
||
- **JavaFX:** https://openjfx.io/openjfx-docs
|
||
- **Gradle:** https://docs.gradle.org/current/userguide/userguide.html
|
||
- **Git:** https://git-scm.com/docs
|
||
- **JUnit:** https://junit.org/junit5/docs/current/user-guide/
|
||
- **Mockito:** https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
|
||
- **EasyMock:** https://easymock.org/user-guide.html
|
||
- **man pages (command line, not online)**
|
||
|
||
<div class="page-break"></div>
|
||
|
||
# DOCKER
|
||
|
||
**Docker reference: https://docs.docker.com/**
|
||
|
||
### Dockerfiles
|
||
https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/
|
||
https://docs.docker.com/reference/dockerfile/
|
||
|
||
```dockerfile
|
||
FROM python:3.13
|
||
WORKDIR /usr/local/app
|
||
|
||
# Install the application dependencies
|
||
COPY requirements.txt ./
|
||
RUN pip install --no-cache-dir -r requirements.txt
|
||
|
||
# Copy in the source code
|
||
COPY src ./src
|
||
EXPOSE 8080
|
||
|
||
# Setup an app user so the container doesn't run as the root user
|
||
RUN useradd app
|
||
USER app
|
||
|
||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||
```
|
||
|
||
### Building docker
|
||
|
||
```bash
|
||
docker build -t <tag>
|
||
```
|
||
|
||
### Docker processes
|
||
```bash
|
||
docker ps
|
||
```
|
||
#### Exercises
|
||
- [L09P01 - Registry Office Service Deployment](file:///home/nikola/Documents/EIST/eist26l09p01-go38coz)
|
||
- [L09P02 - Garching Best Software](file:///home/nikola/Documents/EIST/eist26l09p02-go38coz)
|
||
- [L08PE04 - Spaceship](file:///home/nikola/Documents/EIST/eist26l0pe04-go38coz)
|
||
|
||
<div class="page-break"></div>
|
||
|