From cfb9dd1eb76da4796b56a3512452fd01de27774d Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Mon, 21 Jul 2025 11:00:00 +0800 Subject: [PATCH 1/8] vincent and joshu lab 0.5 --- session1/lab0.5/README.md | 268 ++++++++++++ session1/lab0.5/SUMMARY.md | 392 ++++++++++++++++++ session1/lab0.5/build.gradle | 29 ++ .../gradle/wrapper/gradle-wrapper.properties | 7 + session1/lab0.5/gradlew | 251 +++++++++++ session1/lab0.5/gradlew.bat | 94 +++++ .../com/tddacademy/zoo/ZooApplication.java | 13 + .../java/com/tddacademy/zoo/model/Animal.java | 83 ++++ .../zoo/service/AnimalRepository.java | 22 + .../tddacademy/zoo/service/AnimalService.java | 60 +++ .../zoo/service/NotificationService.java | 24 ++ .../tddacademy/zoo/service/ZooManager.java | 63 +++ .../tddacademy/zoo/ZooApplicationTests.java | 14 + .../zoo/service/MockExamplesTest.java | 210 ++++++++++ .../zoo/service/SpyExamplesTest.java | 199 +++++++++ .../zoo/service/StubExamplesTest.java | 210 ++++++++++ .../zoo/service/TodoExercisesTest.java | 231 +++++++++++ 17 files changed, 2170 insertions(+) create mode 100644 session1/lab0.5/README.md create mode 100644 session1/lab0.5/SUMMARY.md create mode 100644 session1/lab0.5/build.gradle create mode 100644 session1/lab0.5/gradle/wrapper/gradle-wrapper.properties create mode 100755 session1/lab0.5/gradlew create mode 100644 session1/lab0.5/gradlew.bat create mode 100644 session1/lab0.5/src/main/java/com/tddacademy/zoo/ZooApplication.java create mode 100644 session1/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java create mode 100644 session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java create mode 100644 session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java create mode 100644 session1/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java create mode 100644 session1/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java create mode 100644 session1/lab0.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java create mode 100644 session1/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java create mode 100644 session1/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java create mode 100644 session1/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java create mode 100644 session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java diff --git a/session1/lab0.5/README.md b/session1/lab0.5/README.md new file mode 100644 index 0000000..7f83373 --- /dev/null +++ b/session1/lab0.5/README.md @@ -0,0 +1,268 @@ +# Lab 0.5: Test Doubles with Mockito + +## Overview + +Lab 0.5 introduces **Test Doubles** using **Mockito**, a powerful Java testing framework. You'll learn about different types of test doubles: **Mocks**, **Stubs**, and **Spies**, and how to use them effectively in unit testing. + +## Learning Objectives + +By the end of this lab, you will be able to: + +- ✅ **Understand Test Doubles**: Know when and why to use different types +- ✅ **Use Mocks**: Create and configure mocks for behavior verification +- ✅ **Use Stubs**: Provide predefined responses for method calls +- ✅ **Use Spies**: Monitor real object interactions +- ✅ **Write Clean Tests**: Create readable and maintainable test code +- ✅ **Verify Interactions**: Ensure methods are called correctly + +## Prerequisites + +- Java 17 or higher +- Gradle 7.0 or higher +- Basic understanding of JUnit 5 + +## What are Test Doubles? + +Test doubles are objects that replace real dependencies in tests. They help you: + +- **Isolate the unit under test** +- **Control test data and behavior** +- **Verify interactions between components** +- **Speed up test execution** + +## Types of Test Doubles + +### 1. **Mock** 🎭 +- **Purpose**: Verify behavior and interactions +- **Use when**: You want to ensure methods are called correctly +- **Key features**: Behavior verification, interaction tracking + +### 2. **Stub** 📋 +- **Purpose**: Provide predefined responses +- **Use when**: You need specific data for your test +- **Key features**: Return values, no behavior verification + +### 3. **Spy** 👁️ +- **Purpose**: Monitor real object interactions +- **Use when**: You want to track calls to a real object +- **Key features**: Real behavior + verification capabilities + +## Project Structure + +``` +lab0.5/ +├── src/ +│ ├── main/ +│ │ └── java/com/tddacademy/zoo/ +│ │ ├── model/ # Simple Animal model +│ │ └── service/ # Service classes for testing +│ └── test/ +│ └── java/com/tddacademy/zoo/ +│ └── service/ # Test examples and exercises +├── build.gradle +└── README.md +``` + +## Key Mockito Concepts + +### Annotations +- **@Mock**: Creates a mock object +- **@Spy**: Creates a spy object +- **@InjectMocks**: Injects mocks into the class under test +- **@ExtendWith(MockitoExtension.class)**: Enables Mockito support + +### Common Methods +- **when()**: Define mock behavior +- **verify()**: Verify method calls +- **times()**: Specify call count +- **any()**: Match any argument +- **eq()**: Match exact argument + +## Getting Started + +### 1. Run Tests +```bash +./gradlew test +``` + +### 2. Run Specific Test Class +```bash +./gradlew test --tests MockExamplesTest +``` + +### 3. Run TODO Exercises +```bash +./gradlew test --tests TodoExercisesTest +``` + +## Test Examples + +### Mock Examples (12 tests) +- Basic mock setup and verification +- Method call verification +- Argument matching +- Return value configuration + +### Stub Examples (12 tests) +- Predefined data responses +- Empty list handling +- Fixed return values +- Edge case scenarios + +### Spy Examples (10 tests) +- Real object monitoring +- Interaction verification +- Parameter validation +- Call count tracking + +## TODO Exercises + +### Mock Exercises (3 exercises) +1. **Find animal by species** - Mock repository method +2. **Handle animal not found** - Mock empty response +3. **Verify repository save** - Verify method calls + +### Stub Exercises (3 exercises) +1. **Calculate average weight** - Use stub data +2. **Handle empty repository** - Stub empty response +3. **Get animal count** - Stub fixed count + +### Spy Exercises (3 exercises) +1. **Verify email notification** - Spy on notification service +2. **Verify SMS notification** - Spy on SMS sending +3. **Verify no notification** - Spy on healthy animal + +### Advanced Exercises (3 exercises) +1. **Multiple repository calls** - Complex verification +2. **Exact parameter matching** - Precise verification +3. **Complex scenarios** - Multiple mocks and spies + +## Sample Code + +### Basic Mock Setup +```java +@Mock +private AnimalRepository animalRepository; + +@InjectMocks +private AnimalService animalService; + +@Test +void shouldCreateAnimal() { + // Given + Animal animal = new Animal("Simba", "Lion", 180.5, ...); + when(animalRepository.save(any(Animal.class))).thenReturn(animal); + + // When + Animal result = animalService.createAnimal(animal); + + // Then + assertNotNull(result); + verify(animalRepository, times(1)).save(animal); +} +``` + +### Stub with Predefined Data +```java +@Test +void shouldCalculateAverageWeight() { + // Given + List animals = Arrays.asList(simba, nala); + when(animalRepository.findAll()).thenReturn(animals); + + // When + double average = animalService.getAverageWeight(); + + // Then + assertEquals(170.25, average, 0.01); +} +``` + +### Spy for Interaction Verification +```java +@Spy +private NotificationService notificationService; + +@Test +void shouldSendNotification() { + // Given + Animal animal = new Animal("Simba", ...); + + // When + zooManager.addNewAnimal(animal); + + // Then + verify(notificationService).sendEmail( + eq("staff@zoo.com"), + eq("New Animal Added"), + contains("Simba") + ); +} +``` + +## Common Patterns + +### When to Use Each Type + +**Use Mocks when:** +- You need to verify method calls +- You want to ensure interactions happen correctly +- You're testing behavior, not data + +**Use Stubs when:** +- You need specific return values +- You want to test different scenarios +- You're testing data flow + +**Use Spies when:** +- You want to monitor real object behavior +- You need both real behavior and verification +- You're testing integration points + +### Best Practices + +1. **Keep tests simple** - One concept per test +2. **Use descriptive names** - Make test purpose clear +3. **Follow AAA pattern** - Arrange, Act, Assert +4. **Verify only what matters** - Don't over-verify +5. **Use meaningful data** - Make test data realistic + +## Exercise Solutions + +All TODO exercises include detailed comments with step-by-step instructions. Solutions are provided in the test file comments to help you learn the patterns. + +## Next Steps + +After completing Lab 0.5, you'll be ready for: +- **Lab 1**: Basic unit testing with JUnit 5 +- **Lab 1.5**: MockMvc testing basics +- **Lab 2**: Controller testing with MockMvc +- **Lab 3**: JPA persistence testing + +## Troubleshooting + +### Common Issues +1. **Tests not running**: Check @ExtendWith(MockitoExtension.class) +2. **Mocks not working**: Ensure @Mock and @InjectMocks are used correctly +3. **Verification failing**: Check method signatures and arguments + +### Useful Commands +```bash +# Run all tests +./gradlew test + +# Run specific test class +./gradlew test --tests MockExamplesTest + +# Run with debug output +./gradlew test --info + +# Clean and rebuild +./gradlew clean test +``` + +## Resources + +- [Mockito Documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html) +- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) +- [Test Double Patterns](https://martinfowler.com/bliki/TestDouble.html) \ No newline at end of file diff --git a/session1/lab0.5/SUMMARY.md b/session1/lab0.5/SUMMARY.md new file mode 100644 index 0000000..1f96d12 --- /dev/null +++ b/session1/lab0.5/SUMMARY.md @@ -0,0 +1,392 @@ +# Lab 0.5 Summary: Test Doubles with Mockito + +## Test Overview + +Lab 0.5 contains **46 tests** across four test classes, with **34 completed examples** and **12 TODO exercises** for students to complete. + +## Test Breakdown + +### MockExamplesTest (12 tests) - ✅ Completed +**Mock Examples - Behavior Verification:** +1. `shouldCreateAnimalSuccessfully()` - Basic mock setup and verification +2. `shouldFindAnimalByIdWhenExists()` - Mock with return value +3. `shouldReturnEmptyWhenAnimalNotFound()` - Mock with empty response +4. `shouldGetAllAnimals()` - Mock with list return +5. `shouldCalculateAverageWeight()` - Mock with calculation +6. `shouldReturnZeroAverageWeightForEmptyList()` - Mock with empty list +7. `shouldDeleteAnimalWhenExists()` - Mock with void method +8. `shouldReturnFalseWhenDeletingNonExistentAnimal()` - Mock with boolean +9. `shouldCheckIfAnimalIsHealthy()` - Mock with health check +10. `shouldReturnFalseForUnhealthyAnimal()` - Mock with sick animal +11. `shouldReturnFalseForNonExistentAnimal()` - Mock with not found +12. `shouldGetAnimalCount()` - Mock with count + +### StubExamplesTest (12 tests) - ✅ Completed +**Stub Examples - Predefined Responses:** +1. `shouldFindAnimalsBySpeciesUsingStub()` - Stub with species data +2. `shouldReturnEmptyListForNonExistentSpecies()` - Stub with empty list +3. `shouldGetAnimalCountUsingStub()` - Stub with fixed count +4. `shouldCalculateAverageWeightWithStubData()` - Stub with multiple animals +5. `shouldHandleHealthyAnimalCheckWithStub()` - Stub with healthy animal +6. `shouldHandleSickAnimalCheckWithStub()` - Stub with sick animal +7. `shouldCreateAnimalWithStubResponse()` - Stub with saved animal +8. `shouldDeleteAnimalSuccessfullyWithStub()` - Stub with existence check +9. `shouldFailToDeleteNonExistentAnimalWithStub()` - Stub with non-existence +10. `shouldGetAllAnimalsWithStubData()` - Stub with multiple animals +11. `shouldHandleEmptyRepositoryWithStub()` - Stub with empty repository +12. `shouldCalculateZeroAverageForEmptyRepository()` - Stub with zero average + +### SpyExamplesTest (10 tests) - ✅ Completed +**Spy Examples - Real Object Monitoring:** +1. `shouldVerifyNotificationWasSentWhenAddingAnimal()` - Spy on email sending +2. `shouldVerifySMSWasSentWhenRemovingAnimal()` - Spy on SMS sending +3. `shouldVerifyEmailWasSentForUnhealthyAnimal()` - Spy on health alerts +4. `shouldNotSendNotificationForHealthyAnimal()` - Spy on no notification +5. `shouldVerifyNotificationCount()` - Spy on count tracking +6. `shouldVerifyEmailServiceAvailabilityCheck()` - Spy on availability +7. `shouldVerifyMultipleNotificationsForMultipleAnimals()` - Spy on multiple calls +8. `shouldVerifyNotificationParameters()` - Spy on exact parameters +9. `shouldVerifyNoNotificationsForFailedOperations()` - Spy on failure +10. `shouldVerifyNotificationServiceInteraction()` - Spy on interaction + +### TodoExercisesTest (12 tests) - 📝 TODO Exercises +**Student Exercises:** +- **Mock Exercises (3)**: Basic mock usage +- **Stub Exercises (3)**: Predefined data responses +- **Spy Exercises (3)**: Real object monitoring +- **Advanced Exercises (3)**: Complex scenarios + +## TODO Exercise Solutions + +### Mock Exercises + +#### 1. Find Animal by Species +```java +@Test +@DisplayName("TODO: Mock Exercise 1 - Should find animal by species") +void shouldFindAnimalBySpecies() { + // Given + when(animalRepository.findBySpecies("Lion")).thenReturn(Arrays.asList(simba, nala)); + + // When + List lions = animalService.getAnimalsBySpecies("Lion"); + + // Then + assertEquals(2, lions.size()); + assertTrue(lions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); +} +``` + +#### 2. Handle Animal Not Found +```java +@Test +@DisplayName("TODO: Mock Exercise 2 - Should handle animal not found") +void shouldHandleAnimalNotFound() { + // Given + when(animalRepository.findById(999L)).thenReturn(Optional.empty()); + + // When + Optional result = animalService.getAnimalById(999L); + + // Then + assertTrue(result.isEmpty()); +} +``` + +#### 3. Verify Repository Save +```java +@Test +@DisplayName("TODO: Mock Exercise 3 - Should verify repository save was called") +void shouldVerifyRepositorySaveWasCalled() { + // Given + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + // When + animalService.createAnimal(simba); + + // Then + verify(animalRepository, times(1)).save(simba); +} +``` + +### Stub Exercises + +#### 1. Calculate Average Weight +```java +@Test +@DisplayName("TODO: Stub Exercise 1 - Should calculate average weight with stub data") +void shouldCalculateAverageWeightWithStubData() { + // Given + List animals = Arrays.asList(simba, nala, timon); + when(animalRepository.findAll()).thenReturn(animals); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + assertEquals(114.33, averageWeight, 0.01); +} +``` + +#### 2. Handle Empty Repository +```java +@Test +@DisplayName("TODO: Stub Exercise 2 - Should handle empty repository with stub") +void shouldHandleEmptyRepositoryWithStub() { + // Given + when(animalRepository.findAll()).thenReturn(Arrays.asList()); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + assertEquals(0.0, averageWeight, 0.01); +} +``` + +#### 3. Get Animal Count +```java +@Test +@DisplayName("TODO: Stub Exercise 3 - Should get animal count with stub") +void shouldGetAnimalCountWithStub() { + // Given + when(animalRepository.count()).thenReturn(15); + + // When + int count = animalService.getAnimalCount(); + + // Then + assertEquals(15, count); +} +``` + +### Spy Exercises + +#### 1. Verify Email Notification +```java +@Test +@DisplayName("TODO: Spy Exercise 1 - Should verify email notification for new animal") +void shouldVerifyEmailNotificationForNewAnimal() { + // Given + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + // When + zooManager.addNewAnimal(simba); + + // Then + verify(notificationService, times(1)).sendEmail( + eq("staff@zoo.com"), + eq("New Animal Added"), + contains("Simba") + ); +} +``` + +#### 2. Verify SMS Notification +```java +@Test +@DisplayName("TODO: Spy Exercise 2 - Should verify SMS notification for animal removal") +void shouldVerifySMSNotificationForAnimalRemoval() { + // Given + simba.setId(1L); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + when(animalRepository.existsById(1L)).thenReturn(true); + doNothing().when(animalRepository).deleteById(1L); + + // When + zooManager.removeAnimal(1L); + + // Then + verify(notificationService, times(1)).sendSMS( + eq("+1234567890"), + contains("Simba") + ); +} +``` + +#### 3. Verify No Notification +```java +@Test +@DisplayName("TODO: Spy Exercise 3 - Should verify no notification for healthy animal") +void shouldVerifyNoNotificationForHealthyAnimal() { + // Given + simba.setId(1L); + simba.setHealthStatus("Healthy"); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + zooManager.checkAnimalHealth(1L); + + // Then + verify(notificationService, never()).sendEmail(any(), any(), any()); +} +``` + +### Advanced Exercises + +#### 1. Multiple Repository Calls +```java +@Test +@DisplayName("TODO: Advanced Exercise 1 - Should verify multiple repository calls") +void shouldVerifyMultipleRepositoryCalls() { + // Given + List animals = Arrays.asList(simba, nala); + when(animalRepository.findAll()).thenReturn(animals); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + verify(animalRepository, times(1)).findAll(); + assertEquals(170.25, averageWeight, 0.01); +} +``` + +#### 2. Exact Parameter Matching +```java +@Test +@DisplayName("TODO: Advanced Exercise 2 - Should verify notification parameters exactly") +void shouldVerifyNotificationParametersExactly() { + // Given + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + // When + zooManager.addNewAnimal(simba); + + // Then + verify(notificationService).sendEmail( + "staff@zoo.com", + "New Animal Added", + "New animal Simba has been added to the zoo." + ); +} +``` + +#### 3. Complex Scenario +```java +@Test +@DisplayName("TODO: Advanced Exercise 3 - Should handle complex scenario with multiple mocks") +void shouldHandleComplexScenarioWithMultipleMocks() { + // Given + simba.setId(1L); + simba.setHealthStatus("Sick"); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + zooManager.checkAnimalHealth(1L); + + // Then + verify(notificationService, times(1)).sendEmail( + eq("vet@zoo.com"), + eq("Animal Health Alert"), + contains("1") + ); + verify(animalRepository, times(1)).findById(1L); +} +``` + +## Key Learning Points + +### Mock Concepts +- **Behavior Verification**: Ensure methods are called correctly +- **Return Value Configuration**: Control what mocks return +- **Argument Matching**: Use any(), eq(), contains() for flexible matching +- **Call Verification**: Verify method calls with times(), never(), atLeastOnce() + +### Stub Concepts +- **Predefined Data**: Return specific values for testing scenarios +- **Edge Cases**: Handle empty lists, null values, error conditions +- **Data Flow**: Test how data moves through your application +- **Scenario Testing**: Test different business scenarios + +### Spy Concepts +- **Real Object Monitoring**: Track calls to real objects +- **Interaction Verification**: Ensure correct interactions happen +- **Parameter Validation**: Verify exact parameters passed +- **Integration Testing**: Test component interactions + +## Common Patterns + +### Mock Pattern +```java +@Mock +private Dependency dependency; + +@InjectMocks +private ClassUnderTest classUnderTest; + +@Test +void shouldDoSomething() { + // Given + when(dependency.method(any())).thenReturn(expectedValue); + + // When + Result result = classUnderTest.method(); + + // Then + verify(dependency, times(1)).method(any()); + assertEquals(expectedValue, result); +} +``` + +### Stub Pattern +```java +@Test +void shouldHandleScenario() { + // Given + when(repository.findAll()).thenReturn(Arrays.asList(item1, item2)); + + // When + double average = service.calculateAverage(); + + // Then + assertEquals(expectedAverage, average, 0.01); +} +``` + +### Spy Pattern +```java +@Spy +private RealService realService; + +@Test +void shouldInteractCorrectly() { + // Given + Input input = new Input(); + + // When + service.process(input); + + // Then + verify(realService).method(eq(expectedParam)); +} +``` + +## Best Practices + +### When to Use Each Type +- **Mocks**: When you need to verify behavior and interactions +- **Stubs**: When you need predefined responses for testing scenarios +- **Spies**: When you want to monitor real object behavior + +### Test Structure +1. **Arrange**: Set up mocks, stubs, and test data +2. **Act**: Call the method under test +3. **Assert**: Verify results and interactions + +### Verification Guidelines +- Verify only what matters for the test +- Use appropriate matchers (any(), eq(), contains()) +- Check call counts when relevant +- Verify interactions in complex scenarios + +## Next Steps + +After completing Lab 0.5, students will be ready for: +- **Lab 1**: Basic unit testing with JUnit 5 +- **Lab 1.5**: MockMvc testing basics +- **Lab 2**: Controller testing with MockMvc +- **Lab 3**: JPA persistence testing \ No newline at end of file diff --git a/session1/lab0.5/build.gradle b/session1/lab0.5/build.gradle new file mode 100644 index 0000000..622243e --- /dev/null +++ b/session1/lab0.5/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.tddacademy' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties b/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ff23a68 --- /dev/null +++ b/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/session1/lab0.5/gradlew b/session1/lab0.5/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/session1/lab0.5/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/session1/lab0.5/gradlew.bat b/session1/lab0.5/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/session1/lab0.5/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/session1/lab0.5/src/main/java/com/tddacademy/zoo/ZooApplication.java b/session1/lab0.5/src/main/java/com/tddacademy/zoo/ZooApplication.java new file mode 100644 index 0000000..7ebce57 --- /dev/null +++ b/session1/lab0.5/src/main/java/com/tddacademy/zoo/ZooApplication.java @@ -0,0 +1,13 @@ +package com.tddacademy.zoo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ZooApplication { + + public static void main(String[] args) { + SpringApplication.run(ZooApplication.class, args); + } + +} \ No newline at end of file diff --git a/session1/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java b/session1/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java new file mode 100644 index 0000000..42bda42 --- /dev/null +++ b/session1/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java @@ -0,0 +1,83 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public class Animal { + private Long id; + private String name; + private String species; + private Double weight; + private LocalDate dateOfBirth; + private String healthStatus; + + public Animal() {} + + public Animal(String name, String species, Double weight, LocalDate dateOfBirth, String healthStatus) { + this.name = name; + this.species = species; + this.weight = weight; + this.dateOfBirth = dateOfBirth; + this.healthStatus = healthStatus; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSpecies() { + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public Double getWeight() { + return weight; + } + + public void setWeight(Double weight) { + this.weight = weight; + } + + public LocalDate getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } + + public String getHealthStatus() { + return healthStatus; + } + + public void setHealthStatus(String healthStatus) { + this.healthStatus = healthStatus; + } + + @Override + public String toString() { + return "Animal{" + + "id=" + id + + ", name='" + name + '\'' + + ", species='" + species + '\'' + + ", weight=" + weight + + ", dateOfBirth=" + dateOfBirth + + ", healthStatus='" + healthStatus + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java new file mode 100644 index 0000000..8fd68ce --- /dev/null +++ b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java @@ -0,0 +1,22 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import java.util.List; +import java.util.Optional; + +public interface AnimalRepository { + + Animal save(Animal animal); + + Optional findById(Long id); + + List findAll(); + + List findBySpecies(String species); + + void deleteById(Long id); + + boolean existsById(Long id); + + int count(); +} \ No newline at end of file diff --git a/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java new file mode 100644 index 0000000..06846b3 --- /dev/null +++ b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java @@ -0,0 +1,60 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import java.util.List; +import java.util.Optional; + +public class AnimalService { + + private final AnimalRepository animalRepository; + + public AnimalService(AnimalRepository animalRepository) { + this.animalRepository = animalRepository; + } + + public Animal createAnimal(Animal animal) { + return animalRepository.save(animal); + } + + public Optional getAnimalById(Long id) { + return animalRepository.findById(id); + } + + public List getAllAnimals() { + return animalRepository.findAll(); + } + + public List getAnimalsBySpecies(String species) { + return animalRepository.findBySpecies(species); + } + + public boolean deleteAnimal(Long id) { + if (animalRepository.existsById(id)) { + animalRepository.deleteById(id); + return true; + } + return false; + } + + public int getAnimalCount() { + return animalRepository.count(); + } + + public boolean isAnimalHealthy(Long id) { + Optional animal = animalRepository.findById(id); + return animal.map(a -> "Healthy".equals(a.getHealthStatus())).orElse(false); + } + + public double getAverageWeight() { + List animals = animalRepository.findAll(); + if (animals.isEmpty()) { + return 0.0; + } + + double totalWeight = animals.stream() + .mapToDouble(Animal::getWeight) + .sum(); + + return totalWeight / animals.size(); + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java new file mode 100644 index 0000000..f82bf68 --- /dev/null +++ b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java @@ -0,0 +1,24 @@ +package com.tddacademy.zoo.service; + +public class NotificationService { + + public void sendEmail(String to, String subject, String message) { + // This would normally send an email + System.out.println("Email sent to: " + to + " - Subject: " + subject + " - Message: " + message); + } + + public void sendSMS(String phoneNumber, String message) { + // This would normally send an SMS + System.out.println("SMS sent to: " + phoneNumber + " - Message: " + message); + } + + public boolean isEmailServiceAvailable() { + // This would normally check if email service is available + return true; + } + + public int getNotificationCount() { + // This would normally return the count of sent notifications + return 0; + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java new file mode 100644 index 0000000..a9d7a79 --- /dev/null +++ b/session1/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java @@ -0,0 +1,63 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import java.util.List; +import java.util.Optional; + +public class ZooManager { + + private final AnimalService animalService; + private final NotificationService notificationService; + + public ZooManager(AnimalService animalService, NotificationService notificationService) { + this.animalService = animalService; + this.notificationService = notificationService; + } + + public Animal addNewAnimal(Animal animal) { + Animal savedAnimal = animalService.createAnimal(animal); + + // Notify staff about new animal + notificationService.sendEmail("staff@zoo.com", + "New Animal Added", + "New animal " + animal.getName() + " has been added to the zoo."); + + return savedAnimal; + } + + public boolean removeAnimal(Long animalId) { + Optional animal = animalService.getAnimalById(animalId); + + if (animal.isPresent()) { + boolean deleted = animalService.deleteAnimal(animalId); + + if (deleted) { + notificationService.sendSMS("+1234567890", + "Animal " + animal.get().getName() + " has been removed from the zoo."); + return true; + } + } + + return false; + } + + public void checkAnimalHealth(Long animalId) { + if (!animalService.isAnimalHealthy(animalId)) { + notificationService.sendEmail("vet@zoo.com", + "Animal Health Alert", + "Animal with ID " + animalId + " needs medical attention."); + } + } + + public int getTotalAnimals() { + return animalService.getAnimalCount(); + } + + public double getAverageAnimalWeight() { + return animalService.getAverageWeight(); + } + + public List getAnimalsBySpecies(String species) { + return animalService.getAnimalsBySpecies(species); + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java new file mode 100644 index 0000000..0cd018a --- /dev/null +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java @@ -0,0 +1,14 @@ +package com.tddacademy.zoo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZooApplicationTests { + + @Test + void contextLoads() { + // This test verifies that the Spring application context loads successfully + } + +} \ No newline at end of file diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java new file mode 100644 index 0000000..4ec9f54 --- /dev/null +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java @@ -0,0 +1,210 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MockExamplesTest { + + @Mock + private AnimalRepository animalRepository; + + @InjectMocks + private AnimalService animalService; + + private Animal simba; + private Animal nala; + + @BeforeEach + void setUp() { + simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); + nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); + } + + @Test + @DisplayName("Mock Example 1: Should create animal successfully") + void shouldCreateAnimalSuccessfully() { + // Given - Setup the mock behavior + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + // When - Call the method under test + Animal createdAnimal = animalService.createAnimal(simba); + + // Then - Verify the result and mock interaction + assertNotNull(createdAnimal); + assertEquals("Simba", createdAnimal.getName()); + verify(animalRepository, times(1)).save(simba); + } + + @Test + @DisplayName("Mock Example 2: Should find animal by id when exists") + void shouldFindAnimalByIdWhenExists() { + // Given + simba.setId(1L); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + Optional foundAnimal = animalService.getAnimalById(1L); + + // Then + assertTrue(foundAnimal.isPresent()); + assertEquals("Simba", foundAnimal.get().getName()); + } + + @Test + @DisplayName("Mock Example 3: Should return empty when animal not found") + void shouldReturnEmptyWhenAnimalNotFound() { + // Given + when(animalRepository.findById(999L)).thenReturn(Optional.empty()); + + // When + Optional foundAnimal = animalService.getAnimalById(999L); + + // Then + assertTrue(foundAnimal.isEmpty()); + } + + @Test + @DisplayName("Mock Example 4: Should get all animals") + void shouldGetAllAnimals() { + // Given + List animals = Arrays.asList(simba, nala); + when(animalRepository.findAll()).thenReturn(animals); + + // When + List allAnimals = animalService.getAllAnimals(); + + // Then + assertEquals(2, allAnimals.size()); + assertEquals("Simba", allAnimals.get(0).getName()); + assertEquals("Nala", allAnimals.get(1).getName()); + } + + @Test + @DisplayName("Mock Example 5: Should calculate average weight") + void shouldCalculateAverageWeight() { + // Given + List animals = Arrays.asList(simba, nala); + when(animalRepository.findAll()).thenReturn(animals); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + assertEquals(170.25, averageWeight, 0.01); + } + + @Test + @DisplayName("Mock Example 6: Should return zero average weight for empty list") + void shouldReturnZeroAverageWeightForEmptyList() { + // Given + when(animalRepository.findAll()).thenReturn(Arrays.asList()); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + assertEquals(0.0, averageWeight, 0.01); + } + + @Test + @DisplayName("Mock Example 7: Should delete animal when exists") + void shouldDeleteAnimalWhenExists() { + // Given + when(animalRepository.existsById(1L)).thenReturn(true); + doNothing().when(animalRepository).deleteById(1L); + + // When + boolean deleted = animalService.deleteAnimal(1L); + + // Then + assertTrue(deleted); + verify(animalRepository, times(1)).deleteById(1L); + } + + @Test + @DisplayName("Mock Example 8: Should return false when deleting non-existent animal") + void shouldReturnFalseWhenDeletingNonExistentAnimal() { + // Given + when(animalRepository.existsById(999L)).thenReturn(false); + + // When + boolean deleted = animalService.deleteAnimal(999L); + + // Then + assertFalse(deleted); + verify(animalRepository, never()).deleteById(any()); + } + + @Test + @DisplayName("Mock Example 9: Should check if animal is healthy") + void shouldCheckIfAnimalIsHealthy() { + // Given + simba.setId(1L); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + boolean isHealthy = animalService.isAnimalHealthy(1L); + + // Then + assertTrue(isHealthy); + } + + @Test + @DisplayName("Mock Example 10: Should return false for unhealthy animal") + void shouldReturnFalseForUnhealthyAnimal() { + // Given + simba.setId(1L); + simba.setHealthStatus("Sick"); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + boolean isHealthy = animalService.isAnimalHealthy(1L); + + // Then + assertFalse(isHealthy); + } + + @Test + @DisplayName("Mock Example 11: Should return false for non-existent animal") + void shouldReturnFalseForNonExistentAnimal() { + // Given + when(animalRepository.findById(999L)).thenReturn(Optional.empty()); + + // When + boolean isHealthy = animalService.isAnimalHealthy(999L); + + // Then + assertFalse(isHealthy); + } + + @Test + @DisplayName("Mock Example 12: Should get animal count") + void shouldGetAnimalCount() { + // Given + when(animalRepository.count()).thenReturn(5); + + // When + int count = animalService.getAnimalCount(); + + // Then + assertEquals(5, count); + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java new file mode 100644 index 0000000..a129f99 --- /dev/null +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java @@ -0,0 +1,199 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SpyExamplesTest { + + @Mock + private AnimalRepository animalRepository; + + @Mock + private AnimalService animalService; + + @Spy + private NotificationService notificationService; + + @InjectMocks + private ZooManager zooManager; + + private Animal simba; + private Animal nala; + + @BeforeEach + void setUp() { + simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); + nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); + } + + @Test + @DisplayName("Spy Example 1: Should verify notification was sent when adding animal") + void shouldVerifyNotificationWasSentWhenAddingAnimal() { + // Given + simba.setId(1L); + when(animalService.createAnimal(any(Animal.class))).thenReturn(simba); + + // When + Animal addedAnimal = zooManager.addNewAnimal(simba); + + // Then + assertNotNull(addedAnimal); + verify(notificationService, times(1)).sendEmail( + eq("staff@zoo.com"), + eq("New Animal Added"), + contains("Simba") + ); + } + + @Test + @DisplayName("Spy Example 2: Should verify SMS was sent when removing animal") + void shouldVerifySMSWasSentWhenRemovingAnimal() { + // Given + simba.setId(1L); + when(animalService.getAnimalById(1L)).thenReturn(Optional.of(simba)); + when(animalService.deleteAnimal(1L)).thenReturn(true); + + // When + boolean removed = zooManager.removeAnimal(1L); + + // Then + assertTrue(removed); + verify(notificationService, times(1)).sendSMS( + eq("+1234567890"), + contains("Simba") + ); + } + + @Test + @DisplayName("Spy Example 3: Should verify email was sent for unhealthy animal") + void shouldVerifyEmailWasSentForUnhealthyAnimal() { + // Given + simba.setId(1L); + simba.setHealthStatus("Sick"); + when(animalService.isAnimalHealthy(1L)).thenReturn(false); + + // When + zooManager.checkAnimalHealth(1L); + + // Then + verify(notificationService, times(1)).sendEmail( + eq("vet@zoo.com"), + eq("Animal Health Alert"), + contains("1") + ); + } + + @Test + @DisplayName("Spy Example 4: Should not send notification for healthy animal") + void shouldNotSendNotificationForHealthyAnimal() { + // Given + simba.setId(1L); + simba.setHealthStatus("Healthy"); + when(animalService.isAnimalHealthy(1L)).thenReturn(true); + + // When + zooManager.checkAnimalHealth(1L); + + // Then + verify(notificationService, never()).sendEmail(any(), any(), any()); + } + + @Test + @DisplayName("Spy Example 6: Should verify email service availability check") + void shouldVerifyEmailServiceAvailabilityCheck() { + // Given + when(notificationService.isEmailServiceAvailable()).thenReturn(true); + + // When + boolean isAvailable = notificationService.isEmailServiceAvailable(); + + // Then + assertTrue(isAvailable); + verify(notificationService, times(1)).isEmailServiceAvailable(); + } + + @Test + @DisplayName("Spy Example 7: Should verify multiple notifications for multiple animals") + void shouldVerifyMultipleNotificationsForMultipleAnimals() { + // Given + simba.setId(1L); + nala.setId(2L); + when(animalService.createAnimal(any(Animal.class))).thenReturn(simba).thenReturn(nala); + + // When + zooManager.addNewAnimal(simba); + zooManager.addNewAnimal(nala); + + // Then + verify(notificationService, times(2)).sendEmail( + eq("staff@zoo.com"), + eq("New Animal Added"), + any() + ); + } + + @Test + @DisplayName("Spy Example 8: Should verify notification parameters") + void shouldVerifyNotificationParameters() { + // Given + simba.setId(1L); + when(animalService.createAnimal(any(Animal.class))).thenReturn(simba); + + // When + zooManager.addNewAnimal(simba); + + // Then + verify(notificationService).sendEmail( + "staff@zoo.com", + "New Animal Added", + "New animal Simba has been added to the zoo." + ); + } + + @Test + @DisplayName("Spy Example 9: Should verify no notifications for failed operations") + void shouldVerifyNoNotificationsForFailedOperations() { + // Given + when(animalService.getAnimalById(999L)).thenReturn(Optional.empty()); + + // When + boolean removed = zooManager.removeAnimal(999L); + + // Then + assertFalse(removed); + verify(notificationService, never()).sendSMS(any(), any()); + } + + @Test + @DisplayName("Spy Example 10: Should verify notification service interaction") + void shouldVerifyNotificationServiceInteraction() { + // Given + simba.setId(1L); + when(animalService.createAnimal(any(Animal.class))).thenReturn(simba); + + // When + Animal result = zooManager.addNewAnimal(simba); + + // Then + assertNotNull(result); + verify(notificationService, atLeastOnce()).sendEmail(any(), any(), any()); + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java new file mode 100644 index 0000000..bc2275d --- /dev/null +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java @@ -0,0 +1,210 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class StubExamplesTest { + + @Mock + private AnimalRepository animalRepository; + + @InjectMocks + private AnimalService animalService; + + private Animal simba; + private Animal nala; + private Animal timon; + + @BeforeEach + void setUp() { + simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); + nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); + timon = new Animal("Timon", "Meerkat", 2.5, LocalDate.of(2021, 3, 10), "Healthy"); + } + + @Test + @DisplayName("Stub Example 1: Should find animals by species using stub") + void shouldFindAnimalsBySpeciesUsingStub() { + // Given - Create a stub that returns predefined data + List lions = Arrays.asList(simba, nala); + when(animalRepository.findBySpecies("Lion")).thenReturn(lions); + + // When + List foundLions = animalService.getAnimalsBySpecies("Lion"); + + // Then + assertEquals(2, foundLions.size()); + assertTrue(foundLions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); + } + + @Test + @DisplayName("Stub Example 2: Should return empty list for non-existent species") + void shouldReturnEmptyListForNonExistentSpecies() { + // Given - Stub returns empty list + when(animalRepository.findBySpecies("Dragon")).thenReturn(Arrays.asList()); + + // When + List foundAnimals = animalService.getAnimalsBySpecies("Dragon"); + + // Then + assertTrue(foundAnimals.isEmpty()); + } + + @Test + @DisplayName("Stub Example 3: Should get animal count using stub") + void shouldGetAnimalCountUsingStub() { + // Given - Stub returns a fixed count + when(animalRepository.count()).thenReturn(10); + + // When + int count = animalService.getAnimalCount(); + + // Then + assertEquals(10, count); + } + + @Test + @DisplayName("Stub Example 4: Should calculate average weight with stub data") + void shouldCalculateAverageWeightWithStubData() { + // Given - Stub returns predefined animals + List animals = Arrays.asList(simba, nala, timon); + when(animalRepository.findAll()).thenReturn(animals); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + // (180.5 + 160.0 + 2.5) / 3 = 114.33 + assertEquals(114.33, averageWeight, 0.01); + } + + @Test + @DisplayName("Stub Example 5: Should handle healthy animal check with stub") + void shouldHandleHealthyAnimalCheckWithStub() { + // Given - Stub returns a healthy animal + simba.setId(1L); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + boolean isHealthy = animalService.isAnimalHealthy(1L); + + // Then + assertTrue(isHealthy); + } + + @Test + @DisplayName("Stub Example 6: Should handle sick animal check with stub") + void shouldHandleSickAnimalCheckWithStub() { + // Given - Stub returns a sick animal + simba.setId(1L); + simba.setHealthStatus("Sick"); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + // When + boolean isHealthy = animalService.isAnimalHealthy(1L); + + // Then + assertFalse(isHealthy); + } + + @Test + @DisplayName("Stub Example 7: Should create animal with stub response") + void shouldCreateAnimalWithStubResponse() { + // Given - Stub returns the saved animal with ID + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + // When + Animal createdAnimal = animalService.createAnimal(simba); + + // Then + assertNotNull(createdAnimal.getId()); + assertEquals("Simba", createdAnimal.getName()); + } + + @Test + @DisplayName("Stub Example 8: Should delete animal successfully with stub") + void shouldDeleteAnimalSuccessfullyWithStub() { + // Given - Stub confirms animal exists + when(animalRepository.existsById(1L)).thenReturn(true); + doNothing().when(animalRepository).deleteById(1L); + + // When + boolean deleted = animalService.deleteAnimal(1L); + + // Then + assertTrue(deleted); + } + + @Test + @DisplayName("Stub Example 9: Should fail to delete non-existent animal with stub") + void shouldFailToDeleteNonExistentAnimalWithStub() { + // Given - Stub confirms animal doesn't exist + when(animalRepository.existsById(999L)).thenReturn(false); + + // When + boolean deleted = animalService.deleteAnimal(999L); + + // Then + assertFalse(deleted); + } + + @Test + @DisplayName("Stub Example 10: Should get all animals with stub data") + void shouldGetAllAnimalsWithStubData() { + // Given - Stub returns predefined list + List allAnimals = Arrays.asList(simba, nala, timon); + when(animalRepository.findAll()).thenReturn(allAnimals); + + // When + List result = animalService.getAllAnimals(); + + // Then + assertEquals(3, result.size()); + assertEquals("Simba", result.get(0).getName()); + assertEquals("Nala", result.get(1).getName()); + assertEquals("Timon", result.get(2).getName()); + } + + @Test + @DisplayName("Stub Example 11: Should handle empty repository with stub") + void shouldHandleEmptyRepositoryWithStub() { + // Given - Stub returns empty list + when(animalRepository.findAll()).thenReturn(Arrays.asList()); + + // When + List result = animalService.getAllAnimals(); + + // Then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Stub Example 12: Should calculate zero average for empty repository") + void shouldCalculateZeroAverageForEmptyRepository() { + // Given - Stub returns empty list + when(animalRepository.findAll()).thenReturn(Arrays.asList()); + + // When + double averageWeight = animalService.getAverageWeight(); + + // Then + assertEquals(0.0, averageWeight, 0.01); + } +} \ No newline at end of file diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java new file mode 100644 index 0000000..c13c6de --- /dev/null +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java @@ -0,0 +1,231 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Animal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TodoExercisesTest { + + @Mock + private AnimalRepository animalRepository; + + @Spy + private NotificationService notificationService; + + @InjectMocks + private AnimalService animalService; + + @InjectMocks + private ZooManager zooManager; + + private Animal simba; + private Animal nala; + private Animal timon; + + @BeforeEach + void setUp() { + simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); + nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); + timon = new Animal("Timon", "Meerkat", 2.5, LocalDate.of(2021, 3, 10), "Healthy"); + zooManager = new ZooManager(animalService, notificationService); + } + + // ========== MOCK EXERCISES ========== + + @Test + @DisplayName("TODO: Mock Exercise 1 - Should find animal by species") + void shouldFindAnimalBySpecies() { + // TODO: Complete this test using mocks + when(animalRepository.findBySpecies("Lion")).thenReturn(Arrays.asList(simba, nala)); + List lions = animalService.getAnimalsBySpecies("Lion"); + assertEquals(2, lions.size()); + assertTrue(lions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); + } + + @Test + @DisplayName("TODO: Mock Exercise 2 - Should handle animal not found") + void shouldHandleAnimalNotFound() { + // TODO: Complete this test using mocks + + when(animalRepository.findById(999L)).thenReturn(Optional.empty()); + + Optional result = animalService.getAnimalById(999L); + + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("TODO: Mock Exercise 3 - Should verify repository save was called") + void shouldVerifyRepositorySaveWasCalled() { + // TODO: Complete this test using mocks + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + animalService.createAnimal(simba); + + verify(animalRepository, times(1)).save(simba); + } + + // ========== STUB EXERCISES ========== + + @Test + @DisplayName("TODO: Stub Exercise 1 - Should calculate average weight with stub data") + void shouldCalculateAverageWeightWithStubData() { + // TODO: Complete this test using stubs + // 1. Create stub data: simba (180.5), nala (160.0), timon (2.5) + // 2. Mock animalRepository.findAll() to return this stub data + // 3. Call animalService.getAverageWeight() + // 4. Assert the average is 114.33 (with 0.01 precision) + + // Your code here: + // List animals = Arrays.asList(simba, nala, timon); + // when(animalRepository.findAll()).thenReturn(animals); + // + // double averageWeight = animalService.getAverageWeight(); + // + // assertEquals(114.33, averageWeight, 0.01); + } + + @Test + @DisplayName("TODO: Stub Exercise 2 - Should handle empty repository with stub") + void shouldHandleEmptyRepositoryWithStub() { + // TODO: Complete this test using stubs + + when(animalRepository.findAll()).thenReturn(Arrays.asList()); + + double averageWeight = animalService.getAverageWeight(); + + assertEquals(0.0, averageWeight, 0.01); + } + + @Test + @DisplayName("TODO: Stub Exercise 3 - Should get animal count with stub") + void shouldGetAnimalCountWithStub() { + // TODO: Complete this test using stubs + + when(animalRepository.count()).thenReturn(15); + + int count = animalService.getAnimalCount(); + + assertEquals(15, count); + } + + // ========== SPY EXERCISES ========== + + @Test + @DisplayName("TODO: Spy Exercise 1 - Should verify email notification for new animal") + void shouldVerifyEmailNotificationForNewAnimal() { + // TODO: Complete this test using spies + + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + zooManager.addNewAnimal(simba); + + verify(notificationService, times(1)).sendEmail( + eq("staff@zoo.com"), + eq("New Animal Added"), + contains("Simba") + ); + } + + @Test + @DisplayName("TODO: Spy Exercise 2 - Should verify SMS notification for animal removal") + void shouldVerifySMSNotificationForAnimalRemoval() { + // TODO: Complete this test using spies + + simba.setId(1L); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + when(animalRepository.existsById(1L)).thenReturn(true); + doNothing().when(animalRepository).deleteById(1L); + + zooManager.removeAnimal(1L); + + verify(notificationService, times(1)).sendSMS( + eq("+1234567890"), + contains("Simba") + ); + } + + @Test + @DisplayName("TODO: Spy Exercise 3 - Should verify no notification for healthy animal") + void shouldVerifyNoNotificationForHealthyAnimal() { + // TODO: Complete this test using spies + + simba.setId(1L); + simba.setHealthStatus("Healthy"); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + zooManager.checkAnimalHealth(1L); + + verify(notificationService, never()).sendEmail(any(), any(), any()); + } + + // ========== ADVANCED EXERCISES ========== + + @Test + @DisplayName("TODO: Advanced Exercise 1 - Should verify multiple repository calls") + void shouldVerifyMultipleRepositoryCalls() { + // TODO: Complete this test using mocks and verification + + List animals = Arrays.asList(simba, nala); + when(animalRepository.findAll()).thenReturn(animals); + + double averageWeight = animalService.getAverageWeight(); + + verify(animalRepository, times(1)).findAll(); + assertEquals(170.25, averageWeight, 0.01); + } + + @Test + @DisplayName("TODO: Advanced Exercise 2 - Should verify notification parameters exactly") + void shouldVerifyNotificationParametersExactly() { + // TODO: Complete this test using spies and exact parameter matching + + simba.setId(1L); + when(animalRepository.save(any(Animal.class))).thenReturn(simba); + + zooManager.addNewAnimal(simba); + + verify(notificationService).sendEmail( + "staff@zoo.com", + "New Animal Added", + "New animal Simba has been added to the zoo." + ); + } + + @Test + @DisplayName("TODO: Advanced Exercise 3 - Should handle complex scenario with multiple mocks") + void shouldHandleComplexScenarioWithMultipleMocks() { + + simba.setId(1L); + simba.setHealthStatus("Sick"); + when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); + + zooManager.checkAnimalHealth(1L); + + verify(notificationService, times(1)).sendEmail( + eq("vet@zoo.com"), + eq("Animal Health Alert"), + contains("1") + ); + verify(animalRepository, times(1)).findById(1L); + } +} \ No newline at end of file From c4673ed82c04eff058dc54b3bd4dc1f3b1ee1afe Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Mon, 21 Jul 2025 11:19:48 +0800 Subject: [PATCH 2/8] vincent + joshua --- .gitignore | 476 ++++++++++++++++++ lab1.5/README.md | 201 ++++++++ lab1.5/SUMMARY.md | 129 +++++ lab1.5/build.gradle | 27 + .../gradle/wrapper/gradle-wrapper.properties | 7 + lab1.5/gradlew | 242 +++++++++ .../com/tddacademy/zoo/ZooApplication.java | 13 + .../zoo/controller/ZooController.java | 35 ++ .../java/com/tddacademy/zoo/model/Animal.java | 25 + .../com/tddacademy/zoo/model/Enclosure.java | 24 + .../java/com/tddacademy/zoo/model/Person.java | 31 ++ .../java/com/tddacademy/zoo/model/Zoo.java | 21 + .../tddacademy/zoo/service/ZooService.java | 40 ++ .../src/main/resources/application.properties | 13 + .../tddacademy/zoo/ZooApplicationTests.java | 14 + .../zoo/controller/ZooControllerTest.java | 68 +++ lab1/README.md | 114 +++++ lab1/SUMMARY.md | 91 ++++ lab1/build.gradle | 28 ++ lab1/gradle/wrapper/gradle-wrapper.properties | 7 + lab1/gradlew | 242 +++++++++ lab1/gradlew.bat | 89 ++++ .../com/tddacademy/zoo/ZooApplication.java | 13 + .../java/com/tddacademy/zoo/model/Animal.java | 25 + .../com/tddacademy/zoo/model/Enclosure.java | 24 + .../java/com/tddacademy/zoo/model/Person.java | 31 ++ .../java/com/tddacademy/zoo/model/Zoo.java | 21 + .../src/main/resources/application.properties | 13 + .../tddacademy/zoo/ZooApplicationTests.java | 14 + .../com/tddacademy/zoo/model/AnimalTest.java | 148 ++++++ .../tddacademy/zoo/model/EnclosureTest.java | 138 +++++ .../com/tddacademy/zoo/model/PersonTest.java | 192 +++++++ .../com/tddacademy/zoo/model/ZooTest.java | 111 ++++ lab2/README.md | 251 +++++++++ lab2/SUMMARY.md | 205 ++++++++ lab2/build.gradle | 29 ++ lab2/gradle/wrapper/gradle-wrapper.properties | 7 + lab2/gradlew | 242 +++++++++ .../com/tddacademy/zoo/ZooApplication.java | 13 + .../zoo/controller/ZooController.java | 69 +++ .../java/com/tddacademy/zoo/model/Animal.java | 25 + .../com/tddacademy/zoo/model/Enclosure.java | 24 + .../java/com/tddacademy/zoo/model/Person.java | 31 ++ .../java/com/tddacademy/zoo/model/Zoo.java | 21 + .../tddacademy/zoo/service/ZooService.java | 58 +++ .../src/main/resources/application.properties | 16 + .../tddacademy/zoo/ZooApplicationTests.java | 14 + .../zoo/controller/ZooControllerTest.java | 253 ++++++++++ .../zoo/service/ZooServiceTest.java | 175 +++++++ lab3/README.md | 180 +++++++ lab3/SUMMARY.md | 337 +++++++++++++ lab3/build.gradle | 30 ++ lab3/gradle/wrapper/gradle-wrapper.properties | 7 + lab3/gradlew | 242 +++++++++ .../com/tddacademy/zoo/ZooApplication.java | 13 + .../zoo/controller/ZooController.java | 76 +++ .../java/com/tddacademy/zoo/model/Animal.java | 118 +++++ .../com/tddacademy/zoo/model/Enclosure.java | 112 +++++ .../java/com/tddacademy/zoo/model/Person.java | 120 +++++ .../java/com/tddacademy/zoo/model/Zoo.java | 103 ++++ .../zoo/repository/ZooRepository.java | 20 + .../tddacademy/zoo/service/ZooService.java | 65 +++ .../src/main/resources/application.properties | 31 ++ .../tddacademy/zoo/ZooApplicationTests.java | 14 + .../zoo/controller/ZooControllerTest.java | 208 ++++++++ .../zoo/repository/ZooRepositoryTest.java | 155 ++++++ .../zoo/service/ZooServiceTest.java | 212 ++++++++ 67 files changed, 6143 insertions(+) create mode 100644 .gitignore create mode 100644 lab1.5/README.md create mode 100644 lab1.5/SUMMARY.md create mode 100644 lab1.5/build.gradle create mode 100644 lab1.5/gradle/wrapper/gradle-wrapper.properties create mode 100755 lab1.5/gradlew create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java create mode 100644 lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java create mode 100644 lab1.5/src/main/resources/application.properties create mode 100644 lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java create mode 100644 lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java create mode 100644 lab1/README.md create mode 100644 lab1/SUMMARY.md create mode 100644 lab1/build.gradle create mode 100644 lab1/gradle/wrapper/gradle-wrapper.properties create mode 100755 lab1/gradlew create mode 100644 lab1/gradlew.bat create mode 100644 lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java create mode 100644 lab1/src/main/java/com/tddacademy/zoo/model/Animal.java create mode 100644 lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java create mode 100644 lab1/src/main/java/com/tddacademy/zoo/model/Person.java create mode 100644 lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java create mode 100644 lab1/src/main/resources/application.properties create mode 100644 lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java create mode 100644 lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java create mode 100644 lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java create mode 100644 lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java create mode 100644 lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java create mode 100644 lab2/README.md create mode 100644 lab2/SUMMARY.md create mode 100644 lab2/build.gradle create mode 100644 lab2/gradle/wrapper/gradle-wrapper.properties create mode 100755 lab2/gradlew create mode 100644 lab2/src/main/java/com/tddacademy/zoo/ZooApplication.java create mode 100644 lab2/src/main/java/com/tddacademy/zoo/controller/ZooController.java create mode 100644 lab2/src/main/java/com/tddacademy/zoo/model/Animal.java create mode 100644 lab2/src/main/java/com/tddacademy/zoo/model/Enclosure.java create mode 100644 lab2/src/main/java/com/tddacademy/zoo/model/Person.java create mode 100644 lab2/src/main/java/com/tddacademy/zoo/model/Zoo.java create mode 100644 lab2/src/main/java/com/tddacademy/zoo/service/ZooService.java create mode 100644 lab2/src/main/resources/application.properties create mode 100644 lab2/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java create mode 100644 lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java create mode 100644 lab2/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java create mode 100644 lab3/README.md create mode 100644 lab3/SUMMARY.md create mode 100644 lab3/build.gradle create mode 100644 lab3/gradle/wrapper/gradle-wrapper.properties create mode 100755 lab3/gradlew create mode 100644 lab3/src/main/java/com/tddacademy/zoo/ZooApplication.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/controller/ZooController.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/model/Animal.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/model/Enclosure.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/model/Person.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/model/Zoo.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/repository/ZooRepository.java create mode 100644 lab3/src/main/java/com/tddacademy/zoo/service/ZooService.java create mode 100644 lab3/src/main/resources/application.properties create mode 100644 lab3/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java create mode 100644 lab3/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java create mode 100644 lab3/src/test/java/com/tddacademy/zoo/repository/ZooRepositoryTest.java create mode 100644 lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0c8012 --- /dev/null +++ b/.gitignore @@ -0,0 +1,476 @@ +# Compiled class files +*.class + +# Log files +*.log +logs/ + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Virtual machine crash logs +hs_err_pid* +replay_pid* + +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +# IntelliJ IDEA +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +# Eclipse +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +# NetBeans +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +# VS Code +.vscode/ + +# Mac +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.tmp +*.temp +Desktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# Spring Boot +*.pid +*.pid.lock + +# Application specific +application-local.properties +application-dev.properties +application-prod.properties +application-test.properties + +# Database +*.db +*.sqlite +*.sqlite3 +*.h2.db + +# Temporary files +*.tmp +*.temp +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Node.js (if using frontend tools) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python (if using any Python tools) +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Docker +.dockerignore +Dockerfile +docker-compose.yml +docker-compose.override.yml + +# Kubernetes +*.yaml +*.yml +!docker-compose.yml + +# Terraform +*.tfstate +*.tfstate.* +.terraform/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Coverage reports +coverage/ +*.lcov +.coverage +htmlcov/ + +# Test reports +test-results/ +reports/ +*.xml + +# Backup files +*.bak +*.backup +*.old + +# Archive files +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# IDE specific +.vscode/settings.json +.vscode/tasks.json +.vscode/launch.json +.vscode/extensions.json +*.code-workspace + +# Local configuration files +local.properties +local.yml +local.yaml + +# Maven (if migrating from Maven) +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# JVM crash logs +hs_err_pid* + +# Application logs +application.log +spring.log + +# H2 database files +*.h2.db +*.h2.db.trace.db + +# Liquibase +liquibase.properties + +# Flyway +flyway.conf + +# JMeter +*.jmx +*.jtl + +# SonarQube +.sonar/ +sonar-project.properties + +# Checkstyle +checkstyle-result.xml + +# SpotBugs +spotbugsXml.xml + +# JaCoCo +jacoco.exec +*.exec + +# Arquillian +arquillian.xml + +# Spring Boot DevTools +.springBeans + +# Spring Boot Actuator +management/ + +# Spring Boot Configuration Processor +spring-configuration-metadata.json + +# Spring Boot Maven Plugin +spring-boot-maven-plugin + +# Spring Boot Gradle Plugin +spring-boot-gradle-plugin + +# Spring Boot CLI +.spring/ + +# Spring Boot Configuration +spring-boot-configuration-processor + +# Spring Boot Loader +spring-boot-loader + +# Spring Boot Test +spring-boot-starter-test + +# Spring Boot Web +spring-boot-starter-web + +# Spring Boot Data +spring-boot-starter-data-* + +# Spring Boot Security +spring-boot-starter-security + +# Spring Boot Actuator +spring-boot-starter-actuator + +# Spring Boot Validation +spring-boot-starter-validation + +# Spring Boot WebFlux +spring-boot-starter-webflux + +# Spring Boot JPA +spring-boot-starter-data-jpa + +# Spring Boot MongoDB +spring-boot-starter-data-mongodb + +# Spring Boot Redis +spring-boot-starter-data-redis + +# Spring Boot Elasticsearch +spring-boot-starter-data-elasticsearch + +# Spring Boot Cassandra +spring-boot-starter-data-cassandra + +# Spring Boot Neo4j +spring-boot-starter-data-neo4j + +# Spring Boot Solr +spring-boot-starter-data-solr + +# Spring Boot Couchbase +spring-boot-starter-data-couchbase + +# Spring Boot LDAP +spring-boot-starter-data-ldap + +# Spring Boot REST +spring-boot-starter-data-rest + +# Spring Boot HAL +spring-boot-starter-hateoas + +# Spring Boot AMQP +spring-boot-starter-amqp + +# Spring Boot Kafka +spring-boot-starter-kafka + +# Spring Boot Mail +spring-boot-starter-mail + +# Spring Boot Cache +spring-boot-starter-cache + +# Spring Boot Session +spring-boot-starter-session + +# Spring Boot Integration +spring-boot-starter-integration + +# Spring Boot Batch +spring-boot-starter-batch + +# Spring Boot Quartz +spring-boot-starter-quartz + +# Spring Boot Task +spring-boot-starter-task + +# Spring Boot Cloud +spring-cloud-* + +# Spring Boot Config +spring-cloud-config + +# Spring Boot Eureka +spring-cloud-netflix-eureka-* + +# Spring Boot Zuul +spring-cloud-netflix-zuul + +# Spring Boot Hystrix +spring-cloud-netflix-hystrix + +# Spring Boot Ribbon +spring-cloud-netflix-ribbon + +# Spring Boot Feign +spring-cloud-netflix-feign + +# Spring Boot Gateway +spring-cloud-gateway + +# Spring Boot Bus +spring-cloud-bus + +# Spring Boot Stream +spring-cloud-stream + +# Spring Boot Function +spring-cloud-function + +# Spring Boot Task +spring-cloud-task + +# Spring Boot Data Flow +spring-cloud-dataflow + +# Spring Boot Skipper +spring-cloud-skipper + +# Spring Boot Deployer +spring-cloud-deployer + +# Spring Boot Local +spring-cloud-deployer-local + +# Spring Boot Cloud Foundry +spring-cloud-deployer-cloudfoundry + +# Spring Boot Kubernetes +spring-cloud-deployer-kubernetes + +# Spring Boot Mesos +spring-cloud-deployer-mesos + +# Spring Boot YARN +spring-cloud-deployer-yarn + +# Spring Boot Spark +spring-cloud-deployer-spark + +# Spring Boot Data Flow Shell +spring-cloud-dataflow-shell + +# Spring Boot Data Flow UI +spring-cloud-dataflow-ui + +# Spring Boot Data Flow REST Client +spring-cloud-dataflow-rest-client + +# Spring Boot Data Flow REST Resource +spring-cloud-dataflow-rest-resource + +# Spring Boot Data Flow REST Resource Assembler +spring-cloud-dataflow-rest-resource-assembler + +# Spring Boot Data Flow REST Resource Controller +spring-cloud-dataflow-rest-resource-controller + +# Spring Boot Data Flow REST Resource Repository +spring-cloud-dataflow-rest-resource-repository + +# Spring Boot Data Flow REST Resource Service +spring-cloud-dataflow-rest-resource-service + +# Spring Boot Data Flow REST Resource Validator +spring-cloud-dataflow-rest-resource-validator + +# Spring Boot Data Flow REST Resource Assembler +spring-cloud-dataflow-rest-resource-assembler + +# Spring Boot Data Flow REST Resource Controller +spring-cloud-dataflow-rest-resource-controller + +# Spring Boot Data Flow REST Resource Repository +spring-cloud-dataflow-rest-resource-repository + +# Spring Boot Data Flow REST Resource Service +spring-cloud-dataflow-rest-resource-service + +# Spring Boot Data Flow REST Resource Validator +spring-cloud-dataflow-rest-resource-validator \ No newline at end of file diff --git a/lab1.5/README.md b/lab1.5/README.md new file mode 100644 index 0000000..41f05bb --- /dev/null +++ b/lab1.5/README.md @@ -0,0 +1,201 @@ +# Lab 1.5: Introduction to MockMvc Testing + +## Overview +This lab introduces you to MockMvc testing in Spring Boot. You'll learn how to test REST API endpoints using MockMvc without complex Mockito techniques. This is perfect for beginners who want to understand the basics of testing REST APIs. + +## Learning Objectives +- Understand what MockMvc is and why we use it +- Learn how to write simple REST API tests +- Practice testing GET endpoints +- Understand JSON response validation +- Complete exercises to reinforce learning + +## What is MockMvc? +MockMvc is a testing framework that allows you to test Spring MVC controllers without starting a full web server. It simulates HTTP requests and validates responses, making tests fast and reliable. + +## New Features Added + +### 1. **Simple REST Controller** +- `ZooController`: Basic GET endpoints for Zoo operations +- Only GET methods (no POST, PUT, DELETE for simplicity) +- Proper HTTP status codes (200, 404) + +### 2. **Basic Service Layer** +- `ZooService`: Simple service with pre-loaded sample data +- Two sample zoos: Manila Zoo and Cebu Safari +- Basic CRUD operations for demonstration + +### 3. **MockMvc Testing** +- Simple test examples without complex Mockito +- Step-by-step guidance for exercises +- Focus on basic HTTP status and JSON validation + +## API Endpoints + +| Method | Endpoint | Description | Status Codes | +|--------|----------|-------------|--------------| +| GET | `/api/zoos` | Get all zoos | 200 | +| GET | `/api/zoos/{id}` | Get zoo by ID | 200, 404 | + +## Project Structure +``` +lab1.5/ +├── src/ +│ ├── main/ +│ │ ├── java/com/tddacademy/zoo/ +│ │ │ ├── ZooApplication.java +│ │ │ ├── controller/ +│ │ │ │ └── ZooController.java +│ │ │ ├── service/ +│ │ │ │ └── ZooService.java +│ │ │ └── model/ +│ │ │ ├── Zoo.java +│ │ │ ├── Enclosure.java +│ │ │ ├── Animal.java +│ │ │ └── Person.java +│ │ └── resources/ +│ │ └── application.properties +│ └── test/ +│ ├── java/com/tddacademy/zoo/ +│ │ ├── ZooApplicationTests.java +│ │ └── controller/ +│ │ └── ZooControllerTest.java +├── build.gradle +└── README.md +``` + +## Sample Data +The application comes with pre-loaded sample data: +- **Manila Zoo** (ID: 1) - "A beautiful zoo in the heart of Manila" +- **Cebu Safari** (ID: 2) - "World famous safari park" + +## Testing Strategy + +### 1. **Completed Test Example** +- `shouldReturnAllZoos()`: Shows how to test GET /api/zoos +- Demonstrates JSON array validation +- Shows how to check specific array elements + +### 2. **Exercise Tests (TODO)** +- `shouldReturnZooWhenItExists()`: Test GET /api/zoos/{id} for existing zoo +- `shouldReturn404WhenZooDoesNotExist()`: Test GET /api/zoos/{id} for non-existent zoo + +## Key Testing Concepts Demonstrated + +### MockMvc Basic Structure +```java +@WebMvcTest(ZooController.class) +class ZooControllerTest { + @Autowired + private MockMvc mockMvc; + + @Test + void shouldReturnAllZoos() throws Exception { + mockMvc.perform(get("/api/zoos")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray()); + } +} +``` + +### Common MockMvc Expectations +- **`status().isOk()`**: Check for HTTP 200 status +- **`status().isNotFound()`**: Check for HTTP 404 status +- **`content().contentType(MediaType.APPLICATION_JSON)`**: Check content type +- **`jsonPath("$.field").value(expectedValue)`**: Check JSON field values +- **`jsonPath("$").isArray()`**: Check if response is an array +- **`jsonPath("$[0].field").value(expectedValue)`**: Check array element fields + +## How to Run + +### Prerequisites +- Java 17 or higher +- Gradle 8.0 or higher + +### Running the Application +```bash +cd lab1.5 +./gradlew bootRun +``` + +### Running Tests +```bash +cd lab1.5 +./gradlew test +``` + +### Testing the API Manually +```bash +# Get all zoos +curl http://localhost:8080/api/zoos + +# Get a specific zoo +curl http://localhost:8080/api/zoos/1 + +# Try to get a non-existent zoo +curl http://localhost:8080/api/zoos/999 +``` + +## Exercises + +### Exercise 1: Complete the `shouldReturnZooWhenItExists()` Test +**Goal**: Test that GET /api/zoos/1 returns the correct Manila Zoo data. + +**Steps**: +1. Uncomment the mockMvc.perform line +2. Add expectations for: + - HTTP 200 status + - JSON content type + - Zoo ID = 1 + - Zoo name = "Manila Zoo" + - Zoo location = "Manila, Philippines" + - Zoo description = "A beautiful zoo in the heart of Manila" + +**Hint**: Look at the completed test above for examples of how to write expectations. + +### Exercise 2: Complete the `shouldReturn404WhenZooDoesNotExist()` Test +**Goal**: Test that GET /api/zoos/999 returns a 404 status. + +**Steps**: +1. Uncomment the mockMvc.perform line +2. Add expectation for HTTP 404 status + +**Hint**: Use `status().isNotFound()` for 404 status. + +## Expected Test Output +After completing the exercises: +``` +BUILD SUCCESSFUL in Xs +3 actionable tasks: 3 executed +``` + +## Key Learning Points + +### MockMvc Basics +- `@WebMvcTest` annotation for controller testing +- `MockMvc` autowiring for test requests +- `perform()` method to make HTTP requests +- `andExpect()` method to validate responses + +### HTTP Status Codes +- **200 OK**: Successful GET operations +- **404 Not Found**: Resource not found + +### JSON Path Testing +- `$` represents the root of the JSON response +- `$[0]` represents the first element of an array +- `$.fieldName` represents a field in the JSON object + +## Next Steps +After completing this lab, you'll be ready for: +- **Lab 2**: More advanced MockMvc testing with POST, PUT, DELETE +- **Lab 3**: Adding database persistence +- **Lab 4**: Complete CRUD operations +- **Lab 5**: API security + +## Troubleshooting +- Make sure all imports are correct +- Check that the test method throws Exception +- Verify JSON path syntax matches the actual response structure +- Use the manual curl commands to understand the API response format \ No newline at end of file diff --git a/lab1.5/SUMMARY.md b/lab1.5/SUMMARY.md new file mode 100644 index 0000000..5a05ae5 --- /dev/null +++ b/lab1.5/SUMMARY.md @@ -0,0 +1,129 @@ +# Lab 1.5 Summary + +## ✅ What We Accomplished + +### 1. **Simple REST API Implementation** +- Created `ZooController` with basic GET endpoints +- Implemented proper HTTP status codes (200, 404) +- Added JSON response handling +- Simplified error handling + +### 2. **Basic Service Layer** +- Created `ZooService` with pre-loaded sample data +- Two sample zoos: Manila Zoo and Cebu Safari +- Basic CRUD operations for demonstration + +### 3. **MockMvc Testing Introduction** +- Simple test examples without complex Mockito +- Step-by-step guidance for exercises +- Focus on basic HTTP status and JSON validation + +## 🎯 Key Learning Objectives Achieved + +### MockMvc Basics +- ✅ Understanding what MockMvc is and why we use it +- ✅ Learning how to write simple REST API tests +- ✅ Practicing testing GET endpoints +- ✅ Understanding JSON response validation + +### REST API Design +- ✅ Proper HTTP method usage (GET only for simplicity) +- ✅ Consistent URL patterns (`/api/zoos`) +- ✅ Appropriate status codes and responses +- ✅ JSON response handling + +### Testing Patterns +- ✅ `@SpringBootTest` and `@AutoConfigureMockMvc` annotations +- ✅ Basic HTTP status testing +- ✅ JSON path validation +- ✅ Array and object response testing + +## 📊 Test Coverage + +### Completed Test (1 method) +- `shouldReturnAllZoos()`: Demonstrates how to test GET /api/zoos +- Shows JSON array validation +- Shows how to check specific array elements + +### Exercise Tests (2 methods - TODO) +- `shouldReturnZooWhenItExists()`: Test GET /api/zoos/{id} for existing zoo +- `shouldReturn404WhenZooDoesNotExist()`: Test GET /api/zoos/{id} for non-existent zoo + +## 🚀 API Endpoints Implemented + +| Method | Endpoint | Description | Status Codes | +|--------|----------|-------------|--------------| +| GET | `/api/zoos` | Get all zoos | 200 | +| GET | `/api/zoos/{id}` | Get zoo by ID | 200, 404 | + +## 🔧 Technical Implementation + +### Simple REST Controller Pattern +```java +@RestController +@RequestMapping("/api/zoos") +public class ZooController { + // Basic GET endpoints with proper status codes + // Simple error handling for different scenarios +} +``` + +### MockMvc Testing Pattern +```java +@SpringBootTest +@AutoConfigureMockMvc +class ZooControllerTest { + // MockMvc testing with request/response validation + // No complex Mockito techniques +} +``` + +## 📋 What Students Should Understand + +1. **MockMvc Basics**: How to test REST controllers without starting a full web server +2. **HTTP Status Codes**: Understanding 200 OK and 404 Not Found +3. **JSON Path Testing**: How to validate JSON responses using jsonPath +4. **Test Structure**: Given-When-Then pattern in REST API testing +5. **Exercise Completion**: How to follow TODO instructions to complete tests + +## 🔄 Next Steps +This lab provides the foundation for: +- **Lab 2**: More advanced MockMvc testing with POST, PUT, DELETE +- **Lab 3**: Adding database persistence +- **Lab 4**: Complete CRUD operations +- **Lab 5**: API security + +## 🎉 Success Criteria +- [x] REST API endpoints working correctly +- [x] Proper HTTP status codes implemented +- [x] Service layer with sample data +- [x] Basic test coverage (1 completed, 2 exercises) +- [x] Error handling for 404 scenarios +- [x] JSON response handling +- [x] MockMvc testing for REST controllers + +## 📝 Notes for Students +- This lab focuses on simplicity and learning the basics +- No complex Mockito techniques are used +- The exercises are designed to be completed step-by-step +- The API responses are predictable and consistent +- Use the manual curl commands to understand the API behavior before writing tests + +## 🎯 Exercise Solutions + +### Exercise 1: Complete `shouldReturnZooWhenItExists()` +```java +mockMvc.perform(get("/api/zoos/1")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")) + .andExpect(jsonPath("$.location").value("Manila, Philippines")) + .andExpect(jsonPath("$.description").value("A beautiful zoo in the heart of Manila")); +``` + +### Exercise 2: Complete `shouldReturn404WhenZooDoesNotExist()` +```java +mockMvc.perform(get("/api/zoos/999")) + .andExpect(status().isNotFound()); +``` \ No newline at end of file diff --git a/lab1.5/build.gradle b/lab1.5/build.gradle new file mode 100644 index 0000000..59b199b --- /dev/null +++ b/lab1.5/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.tddacademy' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/lab1.5/gradle/wrapper/gradle-wrapper.properties b/lab1.5/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a118ea3 --- /dev/null +++ b/lab1.5/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/lab1.5/gradlew b/lab1.5/gradlew new file mode 100755 index 0000000..4d629e2 --- /dev/null +++ b/lab1.5/gradlew @@ -0,0 +1,242 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Gradle template within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# * treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments). +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java b/lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java new file mode 100644 index 0000000..7ebce57 --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java @@ -0,0 +1,13 @@ +package com.tddacademy.zoo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ZooApplication { + + public static void main(String[] args) { + SpringApplication.run(ZooApplication.class, args); + } + +} \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java b/lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java new file mode 100644 index 0000000..813999a --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java @@ -0,0 +1,35 @@ +package com.tddacademy.zoo.controller; + +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.service.ZooService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/zoos") +public class ZooController { + + private final ZooService zooService; + + public ZooController(ZooService zooService) { + this.zooService = zooService; + } + + @GetMapping + public ResponseEntity> getAllZoos() { + List zoos = zooService.getAllZoos(); + return ResponseEntity.ok(zoos); + } + + @GetMapping("/{id}") + public ResponseEntity getZooById(@PathVariable Long id) { + try { + Zoo zoo = zooService.getZooById(id); + return ResponseEntity.ok(zoo); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } +} \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java b/lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java new file mode 100644 index 0000000..1325024 --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java @@ -0,0 +1,25 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public record Animal( + Long id, + String name, + String species, + String breed, + LocalDate dateOfBirth, + Double weight, + String healthStatus +) { + public Animal { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Animal name cannot be null or empty"); + } + if (species == null || species.trim().isEmpty()) { + throw new IllegalArgumentException("Animal species cannot be null or empty"); + } + if (weight != null && weight <= 0) { + throw new IllegalArgumentException("Animal weight must be positive"); + } + } +} \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java b/lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java new file mode 100644 index 0000000..e916138 --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java @@ -0,0 +1,24 @@ +package com.tddacademy.zoo.model; + +import java.util.List; + +public record Enclosure( + Long id, + String name, + String type, + Double area, + String climate, + List animals +) { + public Enclosure { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Enclosure name cannot be null or empty"); + } + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Enclosure type cannot be null or empty"); + } + if (area != null && area <= 0) { + throw new IllegalArgumentException("Enclosure area must be positive"); + } + } +} \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java b/lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java new file mode 100644 index 0000000..6d062d2 --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java @@ -0,0 +1,31 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public record Person( + Long id, + String firstName, + String lastName, + String role, + String email, + LocalDate hireDate, + Double salary +) { + public Person { + if (firstName == null || firstName.trim().isEmpty()) { + throw new IllegalArgumentException("Person first name cannot be null or empty"); + } + if (lastName == null || lastName.trim().isEmpty()) { + throw new IllegalArgumentException("Person last name cannot be null or empty"); + } + if (role == null || role.trim().isEmpty()) { + throw new IllegalArgumentException("Person role cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Person email cannot be null or empty"); + } + if (salary != null && salary <= 0) { + throw new IllegalArgumentException("Person salary must be positive"); + } + } +} \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java b/lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java new file mode 100644 index 0000000..d9c1180 --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java @@ -0,0 +1,21 @@ +package com.tddacademy.zoo.model; + +import java.util.List; + +public record Zoo( + Long id, + String name, + String location, + String description, + List enclosures, + List people +) { + public Zoo { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Zoo name cannot be null or empty"); + } + if (location == null || location.trim().isEmpty()) { + throw new IllegalArgumentException("Zoo location cannot be null or empty"); + } + } +} \ No newline at end of file diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java b/lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java new file mode 100644 index 0000000..93bc76e --- /dev/null +++ b/lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java @@ -0,0 +1,40 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Zoo; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ZooService { + + private final Map zoos = new HashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public ZooService() { + // Add some sample data for testing + Zoo manilaZoo = new Zoo(1L, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + Zoo cebuSafari = new Zoo(2L, "Cebu Safari", "Cebu, Philippines", + "World famous safari park", new ArrayList<>(), new ArrayList<>()); + + zoos.put(1L, manilaZoo); + zoos.put(2L, cebuSafari); + } + + public List getAllZoos() { + return new ArrayList<>(zoos.values()); + } + + public Zoo getZooById(Long id) { + Zoo zoo = zoos.get(id); + if (zoo == null) { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + return zoo; + } +} \ No newline at end of file diff --git a/lab1.5/src/main/resources/application.properties b/lab1.5/src/main/resources/application.properties new file mode 100644 index 0000000..c82f2ce --- /dev/null +++ b/lab1.5/src/main/resources/application.properties @@ -0,0 +1,13 @@ +# Server Configuration +server.port=8080 + +# Application Name +spring.application.name=zoo-simulator-lab1.5 + +# Logging Configuration +logging.level.com.tddacademy.zoo=INFO +logging.level.org.springframework.web=INFO + +# Jackson Configuration +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.write-dates-as-timestamps=false \ No newline at end of file diff --git a/lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java new file mode 100644 index 0000000..0cd018a --- /dev/null +++ b/lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java @@ -0,0 +1,14 @@ +package com.tddacademy.zoo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZooApplicationTests { + + @Test + void contextLoads() { + // This test verifies that the Spring application context loads successfully + } + +} \ No newline at end of file diff --git a/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java b/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java new file mode 100644 index 0000000..3647305 --- /dev/null +++ b/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java @@ -0,0 +1,68 @@ +package com.tddacademy.zoo.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class ZooControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("GET /api/zoos should return all zoos") + void shouldReturnAllZoos() throws Exception { + // When & Then + mockMvc.perform(get("/api/zoos")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("Manila Zoo")) + .andExpect(jsonPath("$[0].location").value("Manila, Philippines")) + .andExpect(jsonPath("$[1].id").value(2)) + .andExpect(jsonPath("$[1].name").value("Cebu Safari")) + .andExpect(jsonPath("$[1].location").value("Cebu, Philippines")); + } + + @Test + @DisplayName("GET /api/zoos/{id} should return zoo when it exists") + void shouldReturnZooWhenItExists() throws Exception { + // TODO: Complete this test + // 1. Use mockMvc.perform(get("/api/zoos/1")) to make a GET request + // 2. Add expectations for: + // - status().isOk() + // - content().contentType(MediaType.APPLICATION_JSON) + // - jsonPath("$.id").value(1) + // - jsonPath("$.name").value("Manila Zoo") + // - jsonPath("$.location").value("Manila, Philippines") + // - jsonPath("$.description").value("A beautiful zoo in the heart of Manila") + + // Your code here: + // mockMvc.perform(get("/api/zoos/1")) + // .andExpect(...) + // .andExpect(...) + // .andExpect(...); + } + + @Test + @DisplayName("GET /api/zoos/{id} should return 404 when zoo does not exist") + void shouldReturn404WhenZooDoesNotExist() throws Exception { + // TODO: Complete this test + // 1. Use mockMvc.perform(get("/api/zoos/999")) to make a GET request for non-existent zoo + // 2. Add expectation for status().isNotFound() + + // Your code here: + // mockMvc.perform(get("/api/zoos/999")) + // .andExpect(...); + } +} \ No newline at end of file diff --git a/lab1/README.md b/lab1/README.md new file mode 100644 index 0000000..dc68208 --- /dev/null +++ b/lab1/README.md @@ -0,0 +1,114 @@ +# Lab 1: Base Spring Boot Application with Java Records + +## Overview +This lab introduces the fundamentals of Spring Boot 3.5.3 with Java 17 records and basic JUnit testing. We'll create a Zoo Simulator application with domain models using Java records. + +## Learning Objectives +- Set up a basic Spring Boot application +- Understand Java records for immutable data classes +- Write comprehensive JUnit tests +- Learn validation patterns in records +- Understand the relationship between domain models + +## Domain Model +The Zoo Simulator has the following entities: +- **Zoo**: Contains enclosures and people +- **Enclosure**: Contains animals, belongs to a Zoo +- **Animal**: Belongs to an Enclosure +- **Person**: Belongs to a Zoo + +## Project Structure +``` +lab1/ +├── src/ +│ ├── main/ +│ │ ├── java/com/tddacademy/zoo/ +│ │ │ ├── ZooApplication.java +│ │ │ └── model/ +│ │ │ ├── Zoo.java +│ │ │ ├── Enclosure.java +│ │ │ ├── Animal.java +│ │ │ └── Person.java +│ │ └── resources/ +│ │ └── application.properties +│ └── test/ +│ └── java/com/tddacademy/zoo/ +│ ├── ZooApplicationTests.java +│ └── model/ +│ ├── ZooTest.java +│ ├── EnclosureTest.java +│ ├── AnimalTest.java +│ └── PersonTest.java +├── build.gradle +└── README.md +``` + +## Key Features + +### Java Records +- Immutable data classes with built-in validation +- Compact syntax for domain models +- Automatic getter methods +- Built-in `equals()`, `hashCode()`, and `toString()` + +### Validation +Each record includes validation logic: +- Required fields cannot be null or empty +- Numeric values must be positive +- Email validation for Person records + +### Testing +Comprehensive JUnit 5 tests covering: +- Valid object creation +- Validation error scenarios +- Edge cases (null values, negative numbers) + +## How to Run + +### Prerequisites +- Java 17 or higher +- Gradle 8.0 or higher + +### Running the Application +```bash +cd lab1 +./gradlew bootRun +``` + +### Running Tests +```bash +cd lab1 +./gradlew test +``` + +### Building the Application +```bash +cd lab1 +./gradlew build +``` + +## Test Coverage +The application includes tests for: +- **Zoo**: 5 test methods covering creation and validation +- **Enclosure**: 6 test methods covering creation and validation +- **Animal**: 6 test methods covering creation and validation +- **Person**: 7 test methods covering creation and validation + +## Expected Output +When you run the tests, you should see: +``` +BUILD SUCCESSFUL in Xs +24 actionable tasks: 24 executed +``` + +## Next Steps +This lab provides the foundation for: +- Lab 2: Adding REST controllers and CRUD operations +- Lab 3: Implementing JPA persistence +- Lab 4: Complete CRUD for all resources +- Lab 5: API security with API keys + +## Troubleshooting +- Ensure Java 17 is installed and set as JAVA_HOME +- Make sure Gradle is properly installed +- Check that all dependencies are resolved in build.gradle \ No newline at end of file diff --git a/lab1/SUMMARY.md b/lab1/SUMMARY.md new file mode 100644 index 0000000..5434dfb --- /dev/null +++ b/lab1/SUMMARY.md @@ -0,0 +1,91 @@ +# Lab 1 Summary + +## ✅ What We Accomplished + +### 1. **Base Spring Boot Application** +- Created a Spring Boot 3.5.3 application with Java 17 +- Set up Gradle build system with proper dependencies +- Configured application properties for development + +### 2. **Java Records for Domain Models** +- **Zoo**: Contains enclosures and people with validation +- **Enclosure**: Contains animals with area validation +- **Animal**: Animal details with weight validation +- **Person**: Staff information with salary validation + +### 3. **Comprehensive Testing** +- **24 test methods** covering all domain models +- Validation testing for required fields +- Edge case testing (null values, negative numbers) +- Exception testing for invalid data + +### 4. **Project Structure** +``` +lab1/ +├── src/main/java/com/tddacademy/zoo/ +│ ├── ZooApplication.java # Main Spring Boot app +│ └── model/ # Domain models +│ ├── Zoo.java # Zoo record with validation +│ ├── Enclosure.java # Enclosure record with validation +│ ├── Animal.java # Animal record with validation +│ └── Person.java # Person record with validation +├── src/test/java/com/tddacademy/zoo/ +│ ├── ZooApplicationTests.java # Context loading test +│ └── model/ # Unit tests +│ ├── ZooTest.java # 5 test methods +│ ├── EnclosureTest.java # 6 test methods +│ ├── AnimalTest.java # 6 test methods +│ └── PersonTest.java # 7 test methods +├── build.gradle # Gradle configuration +├── gradlew & gradlew.bat # Gradle wrapper scripts +└── README.md # Comprehensive documentation +``` + +## 🎯 Key Learning Objectives Achieved + +### Java Records +- ✅ Immutable data classes with compact syntax +- ✅ Built-in validation using compact constructors +- ✅ Automatic getter methods, equals(), hashCode(), toString() +- ✅ Understanding of record relationships + +### Testing +- ✅ JUnit 5 testing framework +- ✅ Given-When-Then test structure +- ✅ Exception testing with assertThrows +- ✅ Comprehensive validation testing +- ✅ Test naming with @DisplayName + +### Spring Boot +- ✅ Basic Spring Boot application setup +- ✅ Application context loading +- ✅ Configuration properties +- ✅ Gradle build system + +## 🚀 Application Status +- ✅ **Tests Passing**: All 24 tests pass successfully +- ✅ **Application Running**: Spring Boot starts on port 8080 +- ✅ **No REST Endpoints**: This is intentional for Lab 1 +- ✅ **Ready for Lab 2**: Foundation is solid for adding controllers + +## 📋 What Students Should Understand + +1. **Java Records**: How to create immutable domain models with validation +2. **Testing Patterns**: How to write comprehensive unit tests +3. **Spring Boot Basics**: How to set up and run a Spring Boot application +4. **Gradle**: How to manage dependencies and build the project +5. **Domain Modeling**: How to represent business entities and their relationships + +## 🔄 Next Steps +This lab provides the perfect foundation for: +- **Lab 2**: Adding REST controllers and CRUD operations +- **Lab 3**: Implementing JPA persistence with H2 database +- **Lab 4**: Complete CRUD for all resources +- **Lab 5**: API security with API keys + +## 🎉 Success Criteria +- [x] Application compiles and runs +- [x] All tests pass (24/24) +- [x] Domain models are properly validated +- [x] Project structure follows Spring Boot conventions +- [x] Documentation is comprehensive and clear \ No newline at end of file diff --git a/lab1/build.gradle b/lab1/build.gradle new file mode 100644 index 0000000..166e991 --- /dev/null +++ b/lab1/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.tddacademy' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-webflux' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/lab1/gradle/wrapper/gradle-wrapper.properties b/lab1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a118ea3 --- /dev/null +++ b/lab1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/lab1/gradlew b/lab1/gradlew new file mode 100755 index 0000000..8ab98ea --- /dev/null +++ b/lab1/gradlew @@ -0,0 +1,242 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Gradle template within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments). +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/lab1/gradlew.bat b/lab1/gradlew.bat new file mode 100644 index 0000000..272cad2 --- /dev/null +++ b/lab1/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd_ return code when the batch file is called from the command line. +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff --git a/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java b/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java new file mode 100644 index 0000000..7ebce57 --- /dev/null +++ b/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java @@ -0,0 +1,13 @@ +package com.tddacademy.zoo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ZooApplication { + + public static void main(String[] args) { + SpringApplication.run(ZooApplication.class, args); + } + +} \ No newline at end of file diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Animal.java b/lab1/src/main/java/com/tddacademy/zoo/model/Animal.java new file mode 100644 index 0000000..1325024 --- /dev/null +++ b/lab1/src/main/java/com/tddacademy/zoo/model/Animal.java @@ -0,0 +1,25 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public record Animal( + Long id, + String name, + String species, + String breed, + LocalDate dateOfBirth, + Double weight, + String healthStatus +) { + public Animal { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Animal name cannot be null or empty"); + } + if (species == null || species.trim().isEmpty()) { + throw new IllegalArgumentException("Animal species cannot be null or empty"); + } + if (weight != null && weight <= 0) { + throw new IllegalArgumentException("Animal weight must be positive"); + } + } +} \ No newline at end of file diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java b/lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java new file mode 100644 index 0000000..e916138 --- /dev/null +++ b/lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java @@ -0,0 +1,24 @@ +package com.tddacademy.zoo.model; + +import java.util.List; + +public record Enclosure( + Long id, + String name, + String type, + Double area, + String climate, + List animals +) { + public Enclosure { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Enclosure name cannot be null or empty"); + } + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Enclosure type cannot be null or empty"); + } + if (area != null && area <= 0) { + throw new IllegalArgumentException("Enclosure area must be positive"); + } + } +} \ No newline at end of file diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Person.java b/lab1/src/main/java/com/tddacademy/zoo/model/Person.java new file mode 100644 index 0000000..6d062d2 --- /dev/null +++ b/lab1/src/main/java/com/tddacademy/zoo/model/Person.java @@ -0,0 +1,31 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public record Person( + Long id, + String firstName, + String lastName, + String role, + String email, + LocalDate hireDate, + Double salary +) { + public Person { + if (firstName == null || firstName.trim().isEmpty()) { + throw new IllegalArgumentException("Person first name cannot be null or empty"); + } + if (lastName == null || lastName.trim().isEmpty()) { + throw new IllegalArgumentException("Person last name cannot be null or empty"); + } + if (role == null || role.trim().isEmpty()) { + throw new IllegalArgumentException("Person role cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Person email cannot be null or empty"); + } + if (salary != null && salary <= 0) { + throw new IllegalArgumentException("Person salary must be positive"); + } + } +} \ No newline at end of file diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java b/lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java new file mode 100644 index 0000000..d9c1180 --- /dev/null +++ b/lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java @@ -0,0 +1,21 @@ +package com.tddacademy.zoo.model; + +import java.util.List; + +public record Zoo( + Long id, + String name, + String location, + String description, + List enclosures, + List people +) { + public Zoo { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Zoo name cannot be null or empty"); + } + if (location == null || location.trim().isEmpty()) { + throw new IllegalArgumentException("Zoo location cannot be null or empty"); + } + } +} \ No newline at end of file diff --git a/lab1/src/main/resources/application.properties b/lab1/src/main/resources/application.properties new file mode 100644 index 0000000..da0fef8 --- /dev/null +++ b/lab1/src/main/resources/application.properties @@ -0,0 +1,13 @@ +# Server Configuration +server.port=8080 + +# Application Name +spring.application.name=zoo-simulator-lab1 + +# Logging Configuration +logging.level.com.tddacademy.zoo=DEBUG +logging.level.org.springframework.web=DEBUG + +# Jackson Configuration +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.write-dates-as-timestamps=false \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java new file mode 100644 index 0000000..0cd018a --- /dev/null +++ b/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java @@ -0,0 +1,14 @@ +package com.tddacademy.zoo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZooApplicationTests { + + @Test + void contextLoads() { + // This test verifies that the Spring application context loads successfully + } + +} \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java b/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java new file mode 100644 index 0000000..a288762 --- /dev/null +++ b/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java @@ -0,0 +1,148 @@ +package com.tddacademy.zoo.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; + +class AnimalTest { + + @Test + @DisplayName("Should create a valid Animal with all required fields") + void shouldCreateValidAnimal() { + // Given + Long id = 1L; + String name = "Simba"; + String species = "Lion"; + String breed = "African Lion"; + LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + Double weight = 180.5; + String healthStatus = "Healthy"; + + // When + Animal animal = new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus); + + // Then + assertNotNull(animal); + assertEquals(id, animal.id()); + assertEquals(name, animal.name()); + assertEquals(species, animal.species()); + assertEquals(breed, animal.breed()); + assertEquals(dateOfBirth, animal.dateOfBirth()); + assertEquals(weight, animal.weight()); + assertEquals(healthStatus, animal.healthStatus()); + } + + @Test + @DisplayName("Should create animal with null weight") + void shouldCreateAnimalWithNullWeight() { + // Given + Long id = 1L; + String name = "Simba"; + String species = "Lion"; + String breed = "African Lion"; + LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + Double weight = null; + String healthStatus = "Healthy"; + + // When + Animal animal = new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus); + + // Then + assertNotNull(animal); + assertNull(animal.weight()); + } + + @Test + @DisplayName("Should throw exception when animal name is null") + void shouldThrowExceptionWhenNameIsNull() { + // TODO: Complete this test + // 1. Create test data with name = null + // 2. Use assertThrows to test that creating an Animal with null name throws IllegalArgumentException + // 3. Verify the exception message is "Animal name cannot be null or empty" + + // Your code here: + // Long id = 1L; + // String name = null; + // String species = "Lion"; + // String breed = "African Lion"; + // LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + // Double weight = 180.5; + // String healthStatus = "Healthy"; + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + // ); + // assertEquals("Animal name cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when animal species is null") + void shouldThrowExceptionWhenSpeciesIsNull() { + // TODO: Complete this test + // 1. Create test data with species = null + // 2. Use assertThrows to test that creating an Animal with null species throws IllegalArgumentException + // 3. Verify the exception message is "Animal species cannot be null or empty" + + // Your code here: + // Long id = 1L; + // String name = "Simba"; + // String species = null; + // String breed = "African Lion"; + // LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + // Double weight = 180.5; + // String healthStatus = "Healthy"; + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + // ); + // assertEquals("Animal species cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when animal weight is negative") + void shouldThrowExceptionWhenWeightIsNegative() { + // Given + Long id = 1L; + String name = "Simba"; + String species = "Lion"; + String breed = "African Lion"; + LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + Double weight = -50.0; + String healthStatus = "Healthy"; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + ); + assertEquals("Animal weight must be positive", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when animal weight is zero") + void shouldThrowExceptionWhenWeightIsZero() { + // TODO: Complete this test + // 1. Create test data with weight = 0.0 + // 2. Use assertThrows to test that creating an Animal with zero weight throws IllegalArgumentException + // 3. Verify the exception message is "Animal weight must be positive" + + // Your code here: + // Long id = 1L; + // String name = "Simba"; + // String species = "Lion"; + // String breed = "African Lion"; + // LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + // Double weight = 0.0; + // String healthStatus = "Healthy"; + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + // ); + // assertEquals("Animal weight must be positive", exception.getMessage()); + } +} \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java b/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java new file mode 100644 index 0000000..cbf5dd2 --- /dev/null +++ b/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java @@ -0,0 +1,138 @@ +package com.tddacademy.zoo.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +class EnclosureTest { + + @Test + @DisplayName("Should create a valid Enclosure with all required fields") + void shouldCreateValidEnclosure() { + // Given + Long id = 1L; + String name = "Lion Habitat"; + String type = "Savanna"; + Double area = 5000.0; + String climate = "Tropical"; + List animals = new ArrayList<>(); + + // When + Enclosure enclosure = new Enclosure(id, name, type, area, climate, animals); + + // Then + assertNotNull(enclosure); + assertEquals(id, enclosure.id()); + assertEquals(name, enclosure.name()); + assertEquals(type, enclosure.type()); + assertEquals(area, enclosure.area()); + assertEquals(climate, enclosure.climate()); + assertEquals(animals, enclosure.animals()); + } + + @Test + @DisplayName("Should create enclosure with null area") + void shouldCreateEnclosureWithNullArea() { + // Given + Long id = 1L; + String name = "Lion Habitat"; + String type = "Savanna"; + Double area = null; + String climate = "Tropical"; + List animals = new ArrayList<>(); + + // When + Enclosure enclosure = new Enclosure(id, name, type, area, climate, animals); + + // Then + assertNotNull(enclosure); + assertNull(enclosure.area()); + } + + @Test + @DisplayName("Should throw exception when enclosure name is null") + void shouldThrowExceptionWhenNameIsNull() { + // Given + Long id = 1L; + String name = null; + String type = "Savanna"; + Double area = 5000.0; + String climate = "Tropical"; + List animals = new ArrayList<>(); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Enclosure(id, name, type, area, climate, animals) + ); + assertEquals("Enclosure name cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when enclosure type is null") + void shouldThrowExceptionWhenTypeIsNull() { + // TODO: Complete this test + // 1. Create test data with type = null + // 2. Use assertThrows to test that creating an Enclosure with null type throws IllegalArgumentException + // 3. Verify the exception message is "Enclosure type cannot be null or empty" + + // Your code here: + // Long id = 1L; + // String name = "Lion Habitat"; + // String type = null; + // Double area = 5000.0; + // String climate = "Tropical"; + // List animals = new ArrayList<>(); + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Enclosure(id, name, type, area, climate, animals) + // ); + // assertEquals("Enclosure type cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when enclosure area is negative") + void shouldThrowExceptionWhenAreaIsNegative() { + // TODO: Complete this test + // 1. Create test data with area = -100.0 + // 2. Use assertThrows to test that creating an Enclosure with negative area throws IllegalArgumentException + // 3. Verify the exception message is "Enclosure area must be positive" + + // Your code here: + // Long id = 1L; + // String name = "Lion Habitat"; + // String type = "Savanna"; + // Double area = -100.0; + // String climate = "Tropical"; + // List animals = new ArrayList<>(); + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Enclosure(id, name, type, area, climate, animals) + // ); + // assertEquals("Enclosure area must be positive", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when enclosure area is zero") + void shouldThrowExceptionWhenAreaIsZero() { + // Given + Long id = 1L; + String name = "Lion Habitat"; + String type = "Savanna"; + Double area = 0.0; + String climate = "Tropical"; + List animals = new ArrayList<>(); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Enclosure(id, name, type, area, climate, animals) + ); + assertEquals("Enclosure area must be positive", exception.getMessage()); + } +} \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java b/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java new file mode 100644 index 0000000..ccd14f7 --- /dev/null +++ b/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java @@ -0,0 +1,192 @@ +package com.tddacademy.zoo.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; + +class PersonTest { + + @Test + @DisplayName("Should create a valid Person with all required fields") + void shouldCreateValidPerson() { + // TODO: Complete this test + // 1. Create test data with all required fields + // 2. Create a Person object with the test data + // 3. Assert that the person is not null + // 4. Assert that all fields match the expected values + + // Your code here: + // Long id = 1L; + // String firstName = "John"; + // String lastName = "Doe"; + // String role = "Zookeeper"; + // String email = "john.doe@zoo.com"; + // LocalDate hireDate = LocalDate.of(2023, 1, 15); + // Double salary = 45000.0; + // + // Person person = new Person(id, firstName, lastName, role, email, hireDate, salary); + // + // assertNotNull(person); + // assertEquals(id, person.id()); + // assertEquals(firstName, person.firstName()); + // assertEquals(lastName, person.lastName()); + // assertEquals(role, person.role()); + // assertEquals(email, person.email()); + // assertEquals(hireDate, person.hireDate()); + // assertEquals(salary, person.salary()); + } + + @Test + @DisplayName("Should create person with null salary") + void shouldCreatePersonWithNullSalary() { + // Given + Long id = 1L; + String firstName = "John"; + String lastName = "Doe"; + String role = "Zookeeper"; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = null; + + // When + Person person = new Person(id, firstName, lastName, role, email, hireDate, salary); + + // Then + assertNotNull(person); + assertNull(person.salary()); + } + + @Test + @DisplayName("Should throw exception when person first name is null") + void shouldThrowExceptionWhenFirstNameIsNull() { + // TODO: Complete this test + // 1. Create test data with firstName = null + // 2. Use assertThrows to test that creating a Person with null firstName throws IllegalArgumentException + // 3. Verify the exception message is "Person first name cannot be null or empty" + + // Your code here: + // Long id = 1L; + // String firstName = null; + // String lastName = "Doe"; + // String role = "Zookeeper"; + // String email = "john.doe@zoo.com"; + // LocalDate hireDate = LocalDate.of(2023, 1, 15); + // Double salary = 45000.0; + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + // ); + // assertEquals("Person first name cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when person last name is null") + void shouldThrowExceptionWhenLastNameIsNull() { + // Given + Long id = 1L; + String firstName = "John"; + String lastName = null; + String role = "Zookeeper"; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = 45000.0; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + ); + assertEquals("Person last name cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when person role is null") + void shouldThrowExceptionWhenRoleIsNull() { + // TODO: Complete this test + // 1. Create test data with role = null + // 2. Use assertThrows to test that creating a Person with null role throws IllegalArgumentException + // 3. Verify the exception message is "Person role cannot be null or empty" + + // Your code here: + // Long id = 1L; + // String firstName = "John"; + // String lastName = "Doe"; + // String role = null; + // String email = "john.doe@zoo.com"; + // LocalDate hireDate = LocalDate.of(2023, 1, 15); + // Double salary = 45000.0; + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + // ); + // assertEquals("Person role cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when person email is null") + void shouldThrowExceptionWhenEmailIsNull() { + // Given + Long id = 1L; + String firstName = "John"; + String lastName = "Doe"; + String role = "Zookeeper"; + String email = null; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = 45000.0; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + ); + assertEquals("Person email cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when person salary is negative") + void shouldThrowExceptionWhenSalaryIsNegative() { + // Given + Long id = 1L; + String firstName = "John"; + String lastName = "Doe"; + String role = "Zookeeper"; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = -1000.0; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + ); + assertEquals("Person salary must be positive", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when person salary is zero") + void shouldThrowExceptionWhenSalaryIsZero() { + // TODO: Complete this test + // 1. Create test data with salary = 0.0 + // 2. Use assertThrows to test that creating a Person with zero salary throws IllegalArgumentException + // 3. Verify the exception message is "Person salary must be positive" + + // Your code here: + // Long id = 1L; + // String firstName = "John"; + // String lastName = "Doe"; + // String role = "Zookeeper"; + // String email = "john.doe@zoo.com"; + // LocalDate hireDate = LocalDate.of(2023, 1, 15); + // Double salary = 0.0; + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + // ); + // assertEquals("Person salary must be positive", exception.getMessage()); + } +} \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java b/lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java new file mode 100644 index 0000000..9306606 --- /dev/null +++ b/lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java @@ -0,0 +1,111 @@ +package com.tddacademy.zoo.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +class ZooTest { + + @Test + @DisplayName("Should create a valid Zoo with all required fields") + void shouldCreateValidZoo() { + // Given + Long id = 1L; + String name = "Central Park Zoo"; + String location = "New York, NY"; + String description = "A beautiful zoo in the heart of Manhattan"; + List enclosures = new ArrayList<>(); + List people = new ArrayList<>(); + + // When + Zoo zoo = new Zoo(id, name, location, description, enclosures, people); + + // Then + assertNotNull(zoo); + assertEquals(id, zoo.id()); + assertEquals(name, zoo.name()); + assertEquals(location, zoo.location()); + assertEquals(description, zoo.description()); + assertEquals(enclosures, zoo.enclosures()); + assertEquals(people, zoo.people()); + } + + @Test + @DisplayName("Should throw exception when zoo name is null") + void shouldThrowExceptionWhenNameIsNull() { + // Given + Long id = 1L; + String name = null; + String location = "New York, NY"; + String description = "A beautiful zoo in the heart of Manhattan"; + List enclosures = new ArrayList<>(); + List people = new ArrayList<>(); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Zoo(id, name, location, description, enclosures, people) + ); + assertEquals("Zoo name cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when zoo name is empty") + void shouldThrowExceptionWhenNameIsEmpty() { + // Given + Long id = 1L; + String name = ""; + String location = "New York, NY"; + String description = "A beautiful zoo in the heart of Manhattan"; + List enclosures = new ArrayList<>(); + List people = new ArrayList<>(); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Zoo(id, name, location, description, enclosures, people) + ); + assertEquals("Zoo name cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when zoo location is null") + void shouldThrowExceptionWhenLocationIsNull() { + // Given + Long id = 1L; + String name = "Central Park Zoo"; + String location = null; + String description = "A beautiful zoo in the heart of Manhattan"; + List enclosures = new ArrayList<>(); + List people = new ArrayList<>(); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Zoo(id, name, location, description, enclosures, people) + ); + assertEquals("Zoo location cannot be null or empty", exception.getMessage()); + } + + @Test + @DisplayName("Should throw exception when zoo location is empty") + void shouldThrowExceptionWhenLocationIsEmpty() { + // Given + Long id = 1L; + String name = "Central Park Zoo"; + String location = " "; + String description = "A beautiful zoo in the heart of Manhattan"; + List enclosures = new ArrayList<>(); + List people = new ArrayList<>(); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Zoo(id, name, location, description, enclosures, people) + ); + assertEquals("Zoo location cannot be null or empty", exception.getMessage()); + } +} \ No newline at end of file diff --git a/lab2/README.md b/lab2/README.md new file mode 100644 index 0000000..dde4909 --- /dev/null +++ b/lab2/README.md @@ -0,0 +1,251 @@ +# Lab 2: REST API with CRUD Operations and Comprehensive Testing + +## Overview +This lab builds upon Lab 1 by adding a complete REST API for Zoo management with comprehensive request/response testing. Students will learn how to create REST controllers, implement CRUD operations, and test both unit and controller scenarios. + +## Learning Objectives +- Create REST controllers with proper HTTP methods +- Implement service layer for business logic +- Write comprehensive unit tests for services +- Write REST API tests using MockMvc +- Understand HTTP status codes and response handling +- Test error scenarios and edge cases +- Complete exercises to reinforce learning + +## New Features Added + +### 1. **Service Layer** +- `ZooService`: Handles business logic for Zoo operations +- In-memory storage using HashMap +- Proper error handling and validation + +### 2. **REST Controller** +- `ZooController`: Exposes REST endpoints for Zoo CRUD operations +- Proper HTTP status codes (200, 201, 204, 400, 404) +- JSON request/response handling +- Error handling with appropriate responses + +### 3. **Comprehensive Testing** +- **Unit Tests**: Service layer testing +- **Controller Tests**: REST API testing with MockMvc +- **Exercise Tests**: TODO exercises for students to complete + +## API Endpoints + +| Method | Endpoint | Description | Status Codes | +|--------|----------|-------------|--------------| +| GET | `/api/zoos` | Get all zoos | 200 | +| GET | `/api/zoos/{id}` | Get zoo by ID | 200, 404 | +| POST | `/api/zoos` | Create new zoo | 201, 400 | +| PUT | `/api/zoos/{id}` | Update zoo | 200, 400, 404 | +| DELETE | `/api/zoos/{id}` | Delete zoo | 204, 404 | + +## Project Structure +``` +lab2/ +├── src/ +│ ├── main/ +│ │ ├── java/com/tddacademy/zoo/ +│ │ │ ├── ZooApplication.java +│ │ │ ├── controller/ +│ │ │ │ └── ZooController.java +│ │ │ ├── service/ +│ │ │ │ └── ZooService.java +│ │ │ └── model/ +│ │ │ ├── Zoo.java +│ │ │ ├── Enclosure.java +│ │ │ ├── Animal.java +│ │ │ └── Person.java +│ │ └── resources/ +│ │ └── application.properties +│ └── test/ +│ ├── java/com/tddacademy/zoo/ +│ │ ├── ZooApplicationTests.java +│ │ ├── service/ +│ │ │ └── ZooServiceTest.java +│ │ └── controller/ +│ │ └── ZooControllerTest.java +│ └── resources/ +│ └── application-test.properties +├── build.gradle +└── README.md +``` + +## Testing Strategy + +### 1. **Unit Tests (ZooServiceTest)** +- Tests business logic in isolation +- 10 test methods covering all CRUD operations +- Error scenario testing +- Edge case validation + +### 2. **Controller Tests (ZooControllerTest)** +- **Completed Tests**: 7 test methods with full implementation +- **Exercise Tests**: 6 TODO exercises for students to complete +- Request/response validation +- Error handling testing +- Content type validation + +## Key Testing Concepts Demonstrated + +### MockMvc Testing +```java +@WebMvcTest(ZooController.class) +class ZooControllerTest { + @Autowired + private MockMvc mockMvc; + + @Test + void shouldCreateZooSuccessfully() throws Exception { + mockMvc.perform(post("/api/zoos") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(zoo))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("Manila Zoo")); + } +} +``` + +### HTTP Status Code Testing +- **200 OK**: Successful GET and PUT operations +- **201 Created**: Successful POST operations +- **204 No Content**: Successful DELETE operations +- **400 Bad Request**: Invalid input data +- **404 Not Found**: Resource not found + +## How to Run + +### Prerequisites +- Java 17 or higher +- Gradle 8.0 or higher + +### Running the Application +```bash +cd lab2 +./gradlew bootRun +``` + +### Running Tests +```bash +cd lab2 +./gradlew test +``` + +### Testing the API Manually +```bash +# Get all zoos +curl http://localhost:8080/api/zoos + +# Create a zoo +curl -X POST http://localhost:8080/api/zoos \ + -H "Content-Type: application/json" \ + -d '{"name":"Manila Zoo","location":"Manila, Philippines","description":"A beautiful zoo in the heart of Manila"}' + +# Get a specific zoo +curl http://localhost:8080/api/zoos/1 + +# Update a zoo +curl -X PUT http://localhost:8080/api/zoos/1 \ + -H "Content-Type: application/json" \ + -d '{"name":"Updated Zoo","location":"Updated Location","description":"Updated description"}' + +# Delete a zoo +curl -X DELETE http://localhost:8080/api/zoos/1 +``` + +## Test Coverage +- **ZooService**: 10 test methods +- **ZooController**: 7 completed + 6 exercise tests +- **Total**: 17 completed + 6 exercise test methods + +## Expected Test Output +After completing the exercises: +``` +BUILD SUCCESSFUL in Xs +23 actionable tasks: 23 executed +``` + +## Exercises + +### Exercise 1: Complete `shouldReturnZooWhenItExists()` +**Goal**: Test that GET /api/zoos/1 returns the correct Manila Zoo data. + +**Steps**: +1. Set up the mock: `when(zooService.getZooById(1L)).thenReturn(createdZoo);` +2. Use `mockMvc.perform(get("/api/zoos/1"))` to make a GET request +3. Add expectations for HTTP 200, JSON content type, and all zoo fields + +### Exercise 2: Complete `shouldUpdateZooSuccessfully()` +**Goal**: Test that PUT /api/zoos/1 updates a zoo successfully. + +**Steps**: +1. Create an updated zoo object +2. Set up the mock to return the updated zoo +3. Use `mockMvc.perform(put("/api/zoos/1"))` with JSON content +4. Add expectations for HTTP 200 and updated fields + +### Exercise 3: Complete `shouldReturn404WhenUpdatingNonExistentZoo()` +**Goal**: Test that PUT /api/zoos/999 returns a 404 status. + +**Steps**: +1. Set up the mock to throw an exception +2. Use `mockMvc.perform(put("/api/zoos/999"))` with JSON content +3. Add expectation for HTTP 404 + +### Exercise 4: Complete `shouldDeleteZooSuccessfully()` +**Goal**: Test that DELETE /api/zoos/1 deletes a zoo successfully. + +**Steps**: +1. Set up the mock: `doNothing().when(zooService).deleteZoo(1L);` +2. Use `mockMvc.perform(delete("/api/zoos/1"))` to make a DELETE request +3. Add expectation for HTTP 204 (No Content) + +### Exercise 5: Complete `shouldReturn404WhenDeletingNonExistentZoo()` +**Goal**: Test that DELETE /api/zoos/999 returns a 404 status. + +**Steps**: +1. Set up the mock to throw an exception +2. Use `mockMvc.perform(delete("/api/zoos/999"))` to make a DELETE request +3. Add expectation for HTTP 404 + +### Exercise 6: Complete `shouldHandleMalformedJsonInPutRequest()` +**Goal**: Test that PUT with malformed JSON returns a 400 status. + +**Steps**: +1. Use `mockMvc.perform(put("/api/zoos/1"))` with invalid JSON content +2. Add expectation for HTTP 400 (Bad Request) + +## Key Learning Points + +### REST API Design +- Proper HTTP method usage +- Consistent URL patterns +- Appropriate status codes +- JSON request/response handling + +### Testing Patterns +- Given-When-Then structure +- MockMvc for controller testing +- Service layer unit testing +- Error scenario testing +- Content type validation + +### Spring Boot Features +- @RestController annotation +- @RequestMapping for URL mapping +- @RequestBody for JSON deserialization +- ResponseEntity for response control +- Exception handling + +## Next Steps +This lab provides the foundation for: +- **Lab 3**: Adding JPA persistence with H2 database +- **Lab 4**: Complete CRUD for all resources (Enclosures, Animals, People) +- **Lab 5**: API security with API keys + +## Troubleshooting +- Ensure all dependencies are resolved +- Check that the application starts on port 8080 +- Verify that all tests pass before proceeding +- Use the provided curl commands to test the API manually +- Follow the TODO comments in the test file for guidance \ No newline at end of file diff --git a/lab2/SUMMARY.md b/lab2/SUMMARY.md new file mode 100644 index 0000000..5669a67 --- /dev/null +++ b/lab2/SUMMARY.md @@ -0,0 +1,205 @@ +# Lab 2 Summary + +## ✅ What We Accomplished + +### 1. **REST API Implementation** +- Created `ZooController` with full CRUD operations +- Implemented proper HTTP status codes (200, 201, 204, 400, 404) +- Added JSON request/response handling +- Implemented error handling with appropriate responses + +### 2. **Service Layer** +- Created `ZooService` for business logic +- Implemented in-memory storage using HashMap +- Added proper error handling and validation +- Created atomic ID generation + +### 3. **Comprehensive Testing Strategy** +- **Unit Tests**: 10 test methods for service layer +- **Controller Tests**: 7 completed + 6 exercise tests for REST API +- **Total**: 17 completed + 6 exercise test methods + +## 🎯 Key Learning Objectives Achieved + +### REST API Design +- ✅ Proper HTTP method usage (GET, POST, PUT, DELETE) +- ✅ Consistent URL patterns (`/api/zoos`) +- ✅ Appropriate status codes and responses +- ✅ JSON request/response handling +- ✅ Error handling with proper HTTP status codes + +### Testing Patterns +- ✅ MockMvc for controller testing +- ✅ Service layer unit testing +- ✅ Error scenario testing +- ✅ Content type validation +- ✅ Request/response validation +- ✅ Exercise-based learning approach + +### Spring Boot Features +- ✅ @RestController annotation +- ✅ @RequestMapping for URL mapping +- ✅ @RequestBody for JSON deserialization +- ✅ ResponseEntity for response control +- ✅ Exception handling in controllers + +## 📊 Test Coverage + +### ZooService Tests (10 methods) +- Empty list handling +- Zoo creation with validation +- Zoo retrieval by ID +- Zoo updates +- Zoo deletion +- Error scenarios for non-existent resources +- Multiple zoos management + +### ZooController Tests (7 completed + 6 exercises) +**Completed Tests:** +- GET all zoos (empty and populated) +- GET zoo by ID (non-existing) +- POST create zoo (valid data) +- Malformed JSON handling in POST +- Content type validation for all endpoints + +**Exercise Tests (TODO):** +- GET zoo by ID (existing) +- PUT update zoo (valid and non-existing) +- DELETE zoo (existing and non-existing) +- Malformed JSON handling in PUT + +## 🚀 API Endpoints Implemented + +| Method | Endpoint | Description | Status Codes | +|--------|----------|-------------|--------------| +| GET | `/api/zoos` | Get all zoos | 200 | +| GET | `/api/zoos/{id}` | Get zoo by ID | 200, 404 | +| POST | `/api/zoos` | Create new zoo | 201, 400 | +| PUT | `/api/zoos/{id}` | Update zoo | 200, 400, 404 | +| DELETE | `/api/zoos/{id}` | Delete zoo | 204, 404 | + +## 🔧 Technical Implementation + +### Service Layer Pattern +```java +@Service +public class ZooService { + private final Map zoos = new HashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + // CRUD operations with proper error handling +} +``` + +### REST Controller Pattern +```java +@RestController +@RequestMapping("/api/zoos") +public class ZooController { + // HTTP method mappings with proper status codes + // Exception handling for different scenarios +} +``` + +### Testing Patterns +```java +@WebMvcTest(ZooController.class) +class ZooControllerTest { + // MockMvc testing with request/response validation + // TODO exercises for student learning +} +``` + +## 📋 What Students Should Understand + +1. **REST API Design**: How to create proper REST endpoints with correct HTTP methods and status codes +2. **Service Layer**: How to separate business logic from controllers +3. **Testing Strategies**: How to test different layers (unit, controller) +4. **Error Handling**: How to handle exceptions and return appropriate HTTP responses +5. **Request/Response Testing**: How to validate JSON requests and responses +6. **MockMvc**: How to test REST controllers without starting the full application +7. **Exercise Completion**: How to follow TODO instructions to complete tests + +## 🔄 Next Steps +This lab provides the foundation for: +- **Lab 3**: Adding JPA persistence with H2 database +- **Lab 4**: Complete CRUD for all resources (Enclosures, Animals, People) +- **Lab 5**: API security with API keys + +## 🎉 Success Criteria +- [x] REST API endpoints working correctly +- [x] Proper HTTP status codes implemented +- [x] Service layer with business logic +- [x] Comprehensive test coverage (17 completed + 6 exercises) +- [x] Error handling for various scenarios +- [x] JSON request/response handling +- [x] MockMvc testing for REST controllers +- [x] Exercise-based learning approach + +## 📝 Notes for Students +- The application demonstrates proper REST API design patterns +- Tests show how to validate both successful and error scenarios +- MockMvc testing provides confidence in controller behavior +- Error handling ensures robust API behavior +- The service layer pattern separates concerns effectively +- TODO exercises help reinforce learning through hands-on practice + +## 🎯 Exercise Solutions + +### Exercise 1: Complete `shouldReturnZooWhenItExists()` +```java +when(zooService.getZooById(1L)).thenReturn(createdZoo); +mockMvc.perform(get("/api/zoos/1")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")) + .andExpect(jsonPath("$.location").value("Manila, Philippines")) + .andExpect(jsonPath("$.description").value("A beautiful zoo in the heart of Manila")); +``` + +### Exercise 2: Complete `shouldUpdateZooSuccessfully()` +```java +Zoo updatedZoo = new Zoo(1L, "Updated Zoo Name", "Updated Location", "Updated description", new ArrayList<>(), new ArrayList<>()); +when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); +mockMvc.perform(put("/api/zoos/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Updated Zoo Name")) + .andExpect(jsonPath("$.location").value("Updated Location")) + .andExpect(jsonPath("$.description").value("Updated description")); +``` + +### Exercise 3: Complete `shouldReturn404WhenUpdatingNonExistentZoo()` +```java +when(zooService.updateZoo(eq(999L), any(Zoo.class))).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); +mockMvc.perform(put("/api/zoos/999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(status().isNotFound()); +``` + +### Exercise 4: Complete `shouldDeleteZooSuccessfully()` +```java +doNothing().when(zooService).deleteZoo(1L); +mockMvc.perform(delete("/api/zoos/1")) + .andExpect(status().isNoContent()); +``` + +### Exercise 5: Complete `shouldReturn404WhenDeletingNonExistentZoo()` +```java +doThrow(new IllegalArgumentException("Zoo not found with id: 999")).when(zooService).deleteZoo(999L); +mockMvc.perform(delete("/api/zoos/999")) + .andExpect(status().isNotFound()); +``` + +### Exercise 6: Complete `shouldHandleMalformedJsonInPutRequest()` +```java +mockMvc.perform(put("/api/zoos/1") + .contentType(MediaType.APPLICATION_JSON) + .content("{ invalid json }")) + .andExpect(status().isBadRequest()); +``` \ No newline at end of file diff --git a/lab2/build.gradle b/lab2/build.gradle new file mode 100644 index 0000000..df904ff --- /dev/null +++ b/lab2/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.tddacademy' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-webflux' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/lab2/gradle/wrapper/gradle-wrapper.properties b/lab2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a118ea3 --- /dev/null +++ b/lab2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/lab2/gradlew b/lab2/gradlew new file mode 100755 index 0000000..4d629e2 --- /dev/null +++ b/lab2/gradlew @@ -0,0 +1,242 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Gradle template within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# * treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments). +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/ZooApplication.java b/lab2/src/main/java/com/tddacademy/zoo/ZooApplication.java new file mode 100644 index 0000000..7ebce57 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/ZooApplication.java @@ -0,0 +1,13 @@ +package com.tddacademy.zoo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ZooApplication { + + public static void main(String[] args) { + SpringApplication.run(ZooApplication.class, args); + } + +} \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/controller/ZooController.java b/lab2/src/main/java/com/tddacademy/zoo/controller/ZooController.java new file mode 100644 index 0000000..ce48a38 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/controller/ZooController.java @@ -0,0 +1,69 @@ +package com.tddacademy.zoo.controller; + +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.service.ZooService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/zoos") +public class ZooController { + + private final ZooService zooService; + + public ZooController(ZooService zooService) { + this.zooService = zooService; + } + + @GetMapping + public ResponseEntity> getAllZoos() { + List zoos = zooService.getAllZoos(); + return ResponseEntity.ok(zoos); + } + + @GetMapping("/{id}") + public ResponseEntity getZooById(@PathVariable Long id) { + try { + Zoo zoo = zooService.getZooById(id); + return ResponseEntity.ok(zoo); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping + public ResponseEntity createZoo(@RequestBody Zoo zoo) { + try { + Zoo createdZoo = zooService.createZoo(zoo); + return ResponseEntity.status(HttpStatus.CREATED).body(createdZoo); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } + + @PutMapping("/{id}") + public ResponseEntity updateZoo(@PathVariable Long id, @RequestBody Zoo zoo) { + try { + Zoo updatedZoo = zooService.updateZoo(id, zoo); + return ResponseEntity.ok(updatedZoo); + } catch (IllegalArgumentException e) { + if (e.getMessage().contains("not found")) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.badRequest().build(); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteZoo(@PathVariable Long id) { + try { + zooService.deleteZoo(id); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } +} \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/model/Animal.java b/lab2/src/main/java/com/tddacademy/zoo/model/Animal.java new file mode 100644 index 0000000..1325024 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/model/Animal.java @@ -0,0 +1,25 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public record Animal( + Long id, + String name, + String species, + String breed, + LocalDate dateOfBirth, + Double weight, + String healthStatus +) { + public Animal { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Animal name cannot be null or empty"); + } + if (species == null || species.trim().isEmpty()) { + throw new IllegalArgumentException("Animal species cannot be null or empty"); + } + if (weight != null && weight <= 0) { + throw new IllegalArgumentException("Animal weight must be positive"); + } + } +} \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/model/Enclosure.java b/lab2/src/main/java/com/tddacademy/zoo/model/Enclosure.java new file mode 100644 index 0000000..e916138 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/model/Enclosure.java @@ -0,0 +1,24 @@ +package com.tddacademy.zoo.model; + +import java.util.List; + +public record Enclosure( + Long id, + String name, + String type, + Double area, + String climate, + List animals +) { + public Enclosure { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Enclosure name cannot be null or empty"); + } + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Enclosure type cannot be null or empty"); + } + if (area != null && area <= 0) { + throw new IllegalArgumentException("Enclosure area must be positive"); + } + } +} \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/model/Person.java b/lab2/src/main/java/com/tddacademy/zoo/model/Person.java new file mode 100644 index 0000000..6d062d2 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/model/Person.java @@ -0,0 +1,31 @@ +package com.tddacademy.zoo.model; + +import java.time.LocalDate; + +public record Person( + Long id, + String firstName, + String lastName, + String role, + String email, + LocalDate hireDate, + Double salary +) { + public Person { + if (firstName == null || firstName.trim().isEmpty()) { + throw new IllegalArgumentException("Person first name cannot be null or empty"); + } + if (lastName == null || lastName.trim().isEmpty()) { + throw new IllegalArgumentException("Person last name cannot be null or empty"); + } + if (role == null || role.trim().isEmpty()) { + throw new IllegalArgumentException("Person role cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Person email cannot be null or empty"); + } + if (salary != null && salary <= 0) { + throw new IllegalArgumentException("Person salary must be positive"); + } + } +} \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/model/Zoo.java b/lab2/src/main/java/com/tddacademy/zoo/model/Zoo.java new file mode 100644 index 0000000..d9c1180 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/model/Zoo.java @@ -0,0 +1,21 @@ +package com.tddacademy.zoo.model; + +import java.util.List; + +public record Zoo( + Long id, + String name, + String location, + String description, + List enclosures, + List people +) { + public Zoo { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Zoo name cannot be null or empty"); + } + if (location == null || location.trim().isEmpty()) { + throw new IllegalArgumentException("Zoo location cannot be null or empty"); + } + } +} \ No newline at end of file diff --git a/lab2/src/main/java/com/tddacademy/zoo/service/ZooService.java b/lab2/src/main/java/com/tddacademy/zoo/service/ZooService.java new file mode 100644 index 0000000..5d8c628 --- /dev/null +++ b/lab2/src/main/java/com/tddacademy/zoo/service/ZooService.java @@ -0,0 +1,58 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Zoo; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ZooService { + + private final Map zoos = new HashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public List getAllZoos() { + return new ArrayList<>(zoos.values()); + } + + public Zoo getZooById(Long id) { + Zoo zoo = zoos.get(id); + if (zoo == null) { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + return zoo; + } + + public Zoo createZoo(Zoo zoo) { + Long newId = idGenerator.getAndIncrement(); + Zoo newZoo = new Zoo(newId, zoo.name(), zoo.location(), zoo.description(), + zoo.enclosures(), zoo.people()); + zoos.put(newId, newZoo); + return newZoo; + } + + public Zoo updateZoo(Long id, Zoo zoo) { + if (!zoos.containsKey(id)) { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + Zoo updatedZoo = new Zoo(id, zoo.name(), zoo.location(), zoo.description(), + zoo.enclosures(), zoo.people()); + zoos.put(id, updatedZoo); + return updatedZoo; + } + + public void deleteZoo(Long id) { + if (!zoos.containsKey(id)) { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + zoos.remove(id); + } + + public boolean zooExists(Long id) { + return zoos.containsKey(id); + } +} \ No newline at end of file diff --git a/lab2/src/main/resources/application.properties b/lab2/src/main/resources/application.properties new file mode 100644 index 0000000..b0ece9b --- /dev/null +++ b/lab2/src/main/resources/application.properties @@ -0,0 +1,16 @@ +# Server Configuration +server.port=8080 + +# Application Name +spring.application.name=zoo-simulator-lab2 + +# Logging Configuration +logging.level.com.tddacademy.zoo=DEBUG +logging.level.org.springframework.web=DEBUG + +# Jackson Configuration +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.write-dates-as-timestamps=false + +# REST API Configuration +spring.jackson.serialization.fail-on-empty-beans=false \ No newline at end of file diff --git a/lab2/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/lab2/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java new file mode 100644 index 0000000..0cd018a --- /dev/null +++ b/lab2/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java @@ -0,0 +1,14 @@ +package com.tddacademy.zoo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZooApplicationTests { + + @Test + void contextLoads() { + // This test verifies that the Spring application context loads successfully + } + +} \ No newline at end of file diff --git a/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java b/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java new file mode 100644 index 0000000..f54c64b --- /dev/null +++ b/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java @@ -0,0 +1,253 @@ +package com.tddacademy.zoo.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.service.ZooService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ZooController.class) +class ZooControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ZooService zooService; + + @Autowired + private ObjectMapper objectMapper; + + private Zoo testZoo; + private Zoo createdZoo; + + @BeforeEach + void setUp() { + testZoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + createdZoo = new Zoo(1L, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + } + + @Test + @DisplayName("GET /api/zoos should return all zoos") + void shouldReturnAllZoos() throws Exception { + // Given + List zoos = Arrays.asList(createdZoo); + when(zooService.getAllZoos()).thenReturn(zoos); + + // When & Then + mockMvc.perform(get("/api/zoos")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("Manila Zoo")) + .andExpect(jsonPath("$[0].location").value("Manila, Philippines")) + .andExpect(jsonPath("$[0].description").value("A beautiful zoo in the heart of Manila")); + } + + @Test + @DisplayName("GET /api/zoos should return empty list when no zoos exist") + void shouldReturnEmptyListWhenNoZoosExist() throws Exception { + // Given + when(zooService.getAllZoos()).thenReturn(new ArrayList<>()); + + // When & Then + mockMvc.perform(get("/api/zoos")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isEmpty()); + } + + @Test + @DisplayName("GET /api/zoos/{id} should return zoo when it exists") + void shouldReturnZooWhenItExists() throws Exception { + // TODO: Complete this test + // 1. Set up the mock: when(zooService.getZooById(1L)).thenReturn(createdZoo); + // 2. Use mockMvc.perform(get("/api/zoos/1")) to make a GET request + // 3. Add expectations for: + // - status().isOk() + // - content().contentType(MediaType.APPLICATION_JSON) + // - jsonPath("$.id").value(1) + // - jsonPath("$.name").value("Manila Zoo") + // - jsonPath("$.location").value("Manila, Philippines") + // - jsonPath("$.description").value("A beautiful zoo in the heart of Manila") + + // Your code here: + // when(zooService.getZooById(1L)).thenReturn(createdZoo); + // mockMvc.perform(get("/api/zoos/1")) + // .andExpect(...) + // .andExpect(...) + // .andExpect(...); + } + + @Test + @DisplayName("GET /api/zoos/{id} should return 404 when zoo does not exist") + void shouldReturn404WhenZooDoesNotExist() throws Exception { + // Given + when(zooService.getZooById(999L)).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); + + // When & Then + mockMvc.perform(get("/api/zoos/999")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("POST /api/zoos should create zoo successfully") + void shouldCreateZooSuccessfully() throws Exception { + // Given + when(zooService.createZoo(any(Zoo.class))).thenReturn(createdZoo); + + // When & Then + mockMvc.perform(post("/api/zoos") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")) + .andExpect(jsonPath("$.location").value("Manila, Philippines")) + .andExpect(jsonPath("$.description").value("A beautiful zoo in the heart of Manila")); + } + + @Test + @DisplayName("PUT /api/zoos/{id} should update zoo successfully") + void shouldUpdateZooSuccessfully() throws Exception { + // TODO: Complete this test + // 1. Create an updated zoo: new Zoo(1L, "Updated Zoo Name", "Updated Location", "Updated description", new ArrayList<>(), new ArrayList<>()) + // 2. Set up the mock: when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); + // 3. Use mockMvc.perform(put("/api/zoos/1").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(testZoo))) + // 4. Add expectations for: + // - status().isOk() + // - content().contentType(MediaType.APPLICATION_JSON) + // - jsonPath("$.id").value(1) + // - jsonPath("$.name").value("Updated Zoo Name") + // - jsonPath("$.location").value("Updated Location") + // - jsonPath("$.description").value("Updated description") + + // Your code here: + // Zoo updatedZoo = new Zoo(1L, "Updated Zoo Name", "Updated Location", "Updated description", new ArrayList<>(), new ArrayList<>()); + // when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); + // mockMvc.perform(put("/api/zoos/1") + // .contentType(MediaType.APPLICATION_JSON) + // .content(objectMapper.writeValueAsString(testZoo))) + // .andExpect(...) + // .andExpect(...) + // .andExpect(...); + } + + @Test + @DisplayName("PUT /api/zoos/{id} should return 404 when zoo does not exist") + void shouldReturn404WhenUpdatingNonExistentZoo() throws Exception { + // TODO: Complete this test + // 1. Set up the mock to throw an exception: when(zooService.updateZoo(eq(999L), any(Zoo.class))).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); + // 2. Use mockMvc.perform(put("/api/zoos/999").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(testZoo))) + // 3. Add expectation for status().isNotFound() + + // Your code here: + // when(zooService.updateZoo(eq(999L), any(Zoo.class))).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); + // mockMvc.perform(put("/api/zoos/999") + // .contentType(MediaType.APPLICATION_JSON) + // .content(objectMapper.writeValueAsString(testZoo))) + // .andExpect(...); + } + + @Test + @DisplayName("DELETE /api/zoos/{id} should delete zoo successfully") + void shouldDeleteZooSuccessfully() throws Exception { + // TODO: Complete this test + // 1. Set up the mock: doNothing().when(zooService).deleteZoo(1L); + // 2. Use mockMvc.perform(delete("/api/zoos/1")) to make a DELETE request + // 3. Add expectation for status().isNoContent() + + // Your code here: + // doNothing().when(zooService).deleteZoo(1L); + // mockMvc.perform(delete("/api/zoos/1")) + // .andExpect(...); + } + + @Test + @DisplayName("DELETE /api/zoos/{id} should return 404 when zoo does not exist") + void shouldReturn404WhenDeletingNonExistentZoo() throws Exception { + // TODO: Complete this test + // 1. Set up the mock to throw an exception: doThrow(new IllegalArgumentException("Zoo not found with id: 999")).when(zooService).deleteZoo(999L); + // 2. Use mockMvc.perform(delete("/api/zoos/999")) to make a DELETE request + // 3. Add expectation for status().isNotFound() + + // Your code here: + // doThrow(new IllegalArgumentException("Zoo not found with id: 999")).when(zooService).deleteZoo(999L); + // mockMvc.perform(delete("/api/zoos/999")) + // .andExpect(...); + } + + @Test + @DisplayName("Should handle malformed JSON in POST request") + void shouldHandleMalformedJsonInPostRequest() throws Exception { + // When & Then + mockMvc.perform(post("/api/zoos") + .contentType(MediaType.APPLICATION_JSON) + .content("{ invalid json }")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should handle malformed JSON in PUT request") + void shouldHandleMalformedJsonInPutRequest() throws Exception { + // TODO: Complete this test + // 1. Use mockMvc.perform(put("/api/zoos/1").contentType(MediaType.APPLICATION_JSON).content("{ invalid json }")) + // 2. Add expectation for status().isBadRequest() + + // Your code here: + // mockMvc.perform(put("/api/zoos/1") + // .contentType(MediaType.APPLICATION_JSON) + // .content("{ invalid json }")) + // .andExpect(...); + } + + @Test + @DisplayName("Should return proper content type for all responses") + void shouldReturnProperContentTypeForAllResponses() throws Exception { + // Given + when(zooService.getAllZoos()).thenReturn(Arrays.asList(createdZoo)); + when(zooService.getZooById(1L)).thenReturn(createdZoo); + when(zooService.createZoo(any(Zoo.class))).thenReturn(createdZoo); + when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(createdZoo); + doNothing().when(zooService).deleteZoo(1L); + + // When & Then + mockMvc.perform(get("/api/zoos")) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + mockMvc.perform(get("/api/zoos/1")) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + mockMvc.perform(post("/api/zoos") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + mockMvc.perform(put("/api/zoos/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + mockMvc.perform(delete("/api/zoos/1")) + .andExpect(status().isNoContent()); + } +} \ No newline at end of file diff --git a/lab2/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java b/lab2/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java new file mode 100644 index 0000000..e952318 --- /dev/null +++ b/lab2/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java @@ -0,0 +1,175 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Zoo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ZooServiceTest { + + private ZooService zooService; + + @BeforeEach + void setUp() { + zooService = new ZooService(); + } + + @Test + @DisplayName("Should return empty list when no zoos exist") + void shouldReturnEmptyListWhenNoZoosExist() { + // When + List zoos = zooService.getAllZoos(); + + // Then + assertNotNull(zoos); + assertTrue(zoos.isEmpty()); + } + + @Test + @DisplayName("Should create a new zoo successfully") + void shouldCreateNewZooSuccessfully() { + // Given + Zoo zoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + + // When + Zoo createdZoo = zooService.createZoo(zoo); + + // Then + assertNotNull(createdZoo); + assertEquals(1L, createdZoo.id()); + assertEquals("Manila Zoo", createdZoo.name()); + assertEquals("Manila, Philippines", createdZoo.location()); + assertEquals("A beautiful zoo in the heart of Manila", createdZoo.description()); + } + + @Test + @DisplayName("Should get zoo by id successfully") + void shouldGetZooByIdSuccessfully() { + // Given + Zoo zoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + Zoo createdZoo = zooService.createZoo(zoo); + + // When + Zoo foundZoo = zooService.getZooById(createdZoo.id()); + + // Then + assertNotNull(foundZoo); + assertEquals(createdZoo.id(), foundZoo.id()); + assertEquals(createdZoo.name(), foundZoo.name()); + } + + @Test + @DisplayName("Should throw exception when getting non-existent zoo") + void shouldThrowExceptionWhenGettingNonExistentZoo() { + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.getZooById(999L) + ); + assertEquals("Zoo not found with id: 999", exception.getMessage()); + } + + @Test + @DisplayName("Should update zoo successfully") + void shouldUpdateZooSuccessfully() { + // Given + Zoo zoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + Zoo createdZoo = zooService.createZoo(zoo); + + Zoo updatedZooData = new Zoo(null, "Updated Zoo Name", "Updated Location", + "Updated description", new ArrayList<>(), new ArrayList<>()); + + // When + Zoo updatedZoo = zooService.updateZoo(createdZoo.id(), updatedZooData); + + // Then + assertNotNull(updatedZoo); + assertEquals(createdZoo.id(), updatedZoo.id()); + assertEquals("Updated Zoo Name", updatedZoo.name()); + assertEquals("Updated Location", updatedZoo.location()); + assertEquals("Updated description", updatedZoo.description()); + } + + @Test + @DisplayName("Should throw exception when updating non-existent zoo") + void shouldThrowExceptionWhenUpdatingNonExistentZoo() { + // Given + Zoo zoo = new Zoo(null, "Test Zoo", "Test Location", + "Test description", new ArrayList<>(), new ArrayList<>()); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.updateZoo(999L, zoo) + ); + assertEquals("Zoo not found with id: 999", exception.getMessage()); + } + + @Test + @DisplayName("Should delete zoo successfully") + void shouldDeleteZooSuccessfully() { + // Given + Zoo zoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + Zoo createdZoo = zooService.createZoo(zoo); + + // When + zooService.deleteZoo(createdZoo.id()); + + // Then + assertFalse(zooService.zooExists(createdZoo.id())); + assertTrue(zooService.getAllZoos().isEmpty()); + } + + @Test + @DisplayName("Should throw exception when deleting non-existent zoo") + void shouldThrowExceptionWhenDeletingNonExistentZoo() { + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.deleteZoo(999L) + ); + assertEquals("Zoo not found with id: 999", exception.getMessage()); + } + + @Test + @DisplayName("Should return multiple zoos when they exist") + void shouldReturnMultipleZoosWhenTheyExist() { + // Given + Zoo zoo1 = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + Zoo zoo2 = new Zoo(null, "Cebu Safari", "Cebu, Philippines", + "World famous safari park", new ArrayList<>(), new ArrayList<>()); + + // When + zooService.createZoo(zoo1); + zooService.createZoo(zoo2); + List zoos = zooService.getAllZoos(); + + // Then + assertEquals(2, zoos.size()); + assertTrue(zoos.stream().anyMatch(z -> z.name().equals("Manila Zoo"))); + assertTrue(zoos.stream().anyMatch(z -> z.name().equals("Cebu Safari"))); + } + + @Test + @DisplayName("Should check if zoo exists") + void shouldCheckIfZooExists() { + // Given + Zoo zoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + Zoo createdZoo = zooService.createZoo(zoo); + + // When & Then + assertTrue(zooService.zooExists(createdZoo.id())); + assertFalse(zooService.zooExists(999L)); + } +} \ No newline at end of file diff --git a/lab3/README.md b/lab3/README.md new file mode 100644 index 0000000..5ca61fe --- /dev/null +++ b/lab3/README.md @@ -0,0 +1,180 @@ +# Lab 3: JPA Persistence with H2 Database + +## Overview + +Lab 3 introduces **JPA (Java Persistence API)** and database persistence using **H2 in-memory database**. You'll learn how to create JPA entities, repositories, and test database operations. + +## Learning Objectives + +By the end of this lab, you will be able to: + +- ✅ **Understand JPA Entities**: Create and configure JPA entities with annotations +- ✅ **Use Spring Data JPA**: Work with repositories and database operations +- ✅ **Test Database Operations**: Write tests for repository and service layers +- ✅ **Handle Database Relationships**: Understand entity relationships and mappings +- ✅ **Use H2 Database**: Work with in-memory database for testing + +## Prerequisites + +- Java 17 or higher +- Gradle 7.0 or higher +- Understanding of Spring Boot basics (Lab 1 & 2) + +## Project Structure + +``` +lab3/ +├── src/ +│ ├── main/ +│ │ ├── java/com/tddacademy/zoo/ +│ │ │ ├── model/ # JPA Entities +│ │ │ ├── repository/ # Spring Data JPA Repositories +│ │ │ ├── service/ # Business Logic Layer +│ │ │ ├── controller/ # REST Controllers +│ │ │ └── ZooApplication.java +│ │ └── resources/ +│ │ └── application.properties +│ └── test/ +│ └── java/com/tddacademy/zoo/ +│ ├── repository/ # Repository Tests +│ ├── service/ # Service Tests +│ ├── controller/ # Controller Tests +│ └── ZooApplicationTests.java +├── build.gradle +└── README.md +``` + +## Key Concepts + +### 1. JPA Entities +- **@Entity**: Marks a class as a JPA entity +- **@Table**: Specifies the database table name +- **@Id**: Marks a field as the primary key +- **@GeneratedValue**: Configures ID generation strategy +- **@Column**: Configures column properties +- **@OneToMany/@ManyToOne**: Defines relationships + +### 2. Spring Data JPA +- **JpaRepository**: Provides CRUD operations +- **Custom Query Methods**: Define queries by method names +- **@Repository**: Marks a class as a data access component + +### 3. H2 Database +- **In-memory database**: Perfect for testing +- **H2 Console**: Web-based database browser +- **Auto-configuration**: Spring Boot automatically configures H2 + +## Getting Started + +### 1. Run the Application +```bash +./gradlew bootRun +``` + +### 2. Access H2 Console +- URL: http://localhost:8080/h2-console +- JDBC URL: `jdbc:h2:mem:testdb` +- Username: `sa` +- Password: `password` + +### 3. Run Tests +```bash +./gradlew test +``` + +## API Endpoints + +### Zoo Management +- `GET /api/zoos` - Get all zoos +- `GET /api/zoos/{id}` - Get zoo by ID +- `POST /api/zoos` - Create new zoo +- `PUT /api/zoos/{id}` - Update zoo +- `DELETE /api/zoos/{id}` - Delete zoo + +### Search Operations +- `GET /api/zoos/search/name?name={name}` - Search zoos by name +- `GET /api/zoos/search/location?location={location}` - Search zoos by location + +## Test Structure + +### Repository Tests (6 tests) +- ✅ **3 Completed**: Basic CRUD operations +- 📝 **3 TODO**: Custom query methods + +### Service Tests (9 tests) +- ✅ **5 Completed**: Basic service operations +- 📝 **4 TODO**: Update, delete, and error handling + +### Controller Tests (10 tests) +- ✅ **6 Completed**: Basic REST operations +- 📝 **4 TODO**: Update, delete, and error responses + +## TODO Exercises + +### Repository Layer (3 exercises) +1. **Find zoos by name containing** - Test custom query method +2. **Find zoos by location containing** - Test location search +3. **Check existence and delete** - Test existence checks and deletion + +### Service Layer (4 exercises) +1. **Update zoo when exists** - Test update operation +2. **Handle update errors** - Test error handling for non-existent zoo +3. **Delete zoo when exists** - Test delete operation +4. **Handle delete errors** - Test error handling for non-existent zoo + +### Controller Layer (4 exercises) +1. **Update zoo via REST** - Test PUT endpoint +2. **Handle update 404** - Test error response for non-existent zoo +3. **Delete zoo via REST** - Test DELETE endpoint +4. **Handle delete 404** - Test error response for non-existent zoo + +## Sample Data + +The application uses Philippine zoo data: +- **Manila Zoo**: "A beautiful zoo in the heart of Manila" +- **Cebu Safari**: "World famous safari park" + +## Database Schema + +### Zoo Table +- `id` (Primary Key, Auto-generated) +- `name` (Not null) +- `location` (Not null) +- `description` (Text) + +### Relationships +- Zoo → Enclosures (One-to-Many) +- Zoo → People (One-to-Many) +- Enclosure → Animals (One-to-Many) + +## Next Steps + +After completing Lab 3, you'll be ready for: +- **Lab 4**: Complete CRUD for all resources (Animals, Enclosures, People) +- **Lab 5**: API security with API keys +- **Lab 6**: Advanced testing with TestContainers + +## Troubleshooting + +### Common Issues +1. **H2 Console not accessible**: Check if application is running +2. **Tests failing**: Ensure all dependencies are downloaded +3. **Database connection issues**: Verify application.properties configuration + +### Useful Commands +```bash +# Clean and build +./gradlew clean build + +# Run with debug logging +./gradlew bootRun --debug + +# Run specific test class +./gradlew test --tests ZooRepositoryTest +``` + +## Resources + +- [Spring Data JPA Reference](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/) +- [H2 Database Documentation](http://www.h2database.com/html/main.html) +- [JPA Specification](https://jakarta.ee/specifications/persistence/) \ No newline at end of file diff --git a/lab3/SUMMARY.md b/lab3/SUMMARY.md new file mode 100644 index 0000000..9c34f86 --- /dev/null +++ b/lab3/SUMMARY.md @@ -0,0 +1,337 @@ +# Lab 3 Summary: JPA Persistence with H2 Database + +## Test Overview + +Lab 3 contains **25 tests** across three testing layers, with **12 completed tests** and **13 TODO exercises** for students to complete. + +## Test Breakdown + +### Repository Tests (6 tests) +**✅ Completed (3 tests):** +1. `shouldSaveZooSuccessfully()` - Basic save operation +2. `shouldFindZooById()` - Find by ID operation +3. `shouldFindAllZoos()` - Find all operation + +**📝 TODO Exercises (3 tests):** +1. `shouldFindZoosByNameContaining()` - Custom query method +2. `shouldFindZoosByLocationContaining()` - Location search +3. `shouldCheckIfZooExistsById()` - Existence checks +4. `shouldDeleteZooById()` - Delete operation + +### Service Tests (9 tests) +**✅ Completed (5 tests):** +1. `shouldGetAllZoos()` - Get all zoos +2. `shouldGetZooByIdWhenExists()` - Get by ID (exists) +3. `shouldReturnEmptyWhenZooNotFound()` - Get by ID (not found) +4. `shouldCreateZooSuccessfully()` - Create operation +5. `shouldFindZoosByName()` - Search by name +6. `shouldCheckIfZooExists()` - Existence check + +**📝 TODO Exercises (4 tests):** +1. `shouldUpdateZooWhenExists()` - Update operation +2. `shouldThrowExceptionWhenUpdatingNonExistentZoo()` - Update error handling +3. `shouldDeleteZooWhenExists()` - Delete operation +4. `shouldThrowExceptionWhenDeletingNonExistentZoo()` - Delete error handling + +### Controller Tests (10 tests) +**✅ Completed (6 tests):** +1. `shouldGetAllZoos()` - GET all zoos +2. `shouldGetZooByIdWhenExists()` - GET by ID (exists) +3. `shouldReturn404WhenZooNotFound()` - GET by ID (404) +4. `shouldCreateZooSuccessfully()` - POST create zoo +5. `shouldSearchZoosByName()` - GET search by name +6. `shouldSearchZoosByLocation()` - GET search by location + +**📝 TODO Exercises (4 tests):** +1. `shouldUpdateZooWhenExists()` - PUT update zoo +2. `shouldReturn404WhenUpdatingNonExistentZoo()` - PUT 404 error +3. `shouldDeleteZooWhenExists()` - DELETE zoo +4. `shouldReturn404WhenDeletingNonExistentZoo()` - DELETE 404 error + +## TODO Exercise Solutions + +### Repository Layer Solutions + +#### 1. Find Zoos by Name Containing +```java +@Test +@DisplayName("Should find zoos by name containing") +void shouldFindZoosByNameContaining() { + // Given + zooRepository.save(manilaZoo); + zooRepository.save(cebuSafari); + + // When + List manilaZoos = zooRepository.findByNameContainingIgnoreCase("Manila"); + + // Then + assertEquals(1, manilaZoos.size()); + assertEquals("Manila Zoo", manilaZoos.get(0).getName()); +} +``` + +#### 2. Find Zoos by Location Containing +```java +@Test +@DisplayName("Should find zoos by location containing") +void shouldFindZoosByLocationContaining() { + // Given + zooRepository.save(manilaZoo); + zooRepository.save(cebuSafari); + + // When + List philippineZoos = zooRepository.findByLocationContainingIgnoreCase("Philippines"); + + // Then + assertEquals(2, philippineZoos.size()); + assertTrue(philippineZoos.stream().anyMatch(zoo -> zoo.getName().equals("Manila Zoo"))); + assertTrue(philippineZoos.stream().anyMatch(zoo -> zoo.getName().equals("Cebu Safari"))); +} +``` + +#### 3. Check Existence and Delete +```java +@Test +@DisplayName("Should check if zoo exists by id") +void shouldCheckIfZooExistsById() { + // Given + Zoo savedZoo = zooRepository.save(manilaZoo); + Long savedId = savedZoo.getId(); + + // When & Then + assertTrue(zooRepository.existsById(savedId)); + assertFalse(zooRepository.existsById(999L)); +} + +@Test +@DisplayName("Should delete zoo by id") +void shouldDeleteZooById() { + // Given + Zoo savedZoo = zooRepository.save(manilaZoo); + Long savedId = savedZoo.getId(); + + // When + zooRepository.deleteById(savedId); + Optional deletedZoo = zooRepository.findById(savedId); + + // Then + assertTrue(deletedZoo.isEmpty()); +} +``` + +### Service Layer Solutions + +#### 1. Update Zoo When Exists +```java +@Test +@DisplayName("Should update zoo when exists") +void shouldUpdateZooWhenExists() { + // Given + Long zooId = 1L; + manilaZoo.setId(zooId); + Zoo updatedZoo = new Zoo("Updated Manila Zoo", "Updated Location", "Updated description"); + + when(zooRepository.findById(zooId)).thenReturn(Optional.of(manilaZoo)); + when(zooRepository.save(any(Zoo.class))).thenReturn(updatedZoo); + + // When + Zoo result = zooService.updateZoo(zooId, updatedZoo); + + // Then + assertEquals("Updated Manila Zoo", result.getName()); + verify(zooRepository, times(1)).save(any(Zoo.class)); +} +``` + +#### 2. Handle Update Errors +```java +@Test +@DisplayName("Should throw exception when updating non-existent zoo") +void shouldThrowExceptionWhenUpdatingNonExistentZoo() { + // Given + Long zooId = 999L; + Zoo updatedZoo = new Zoo("Updated Zoo", "Updated Location", "Updated description"); + + when(zooRepository.findById(zooId)).thenReturn(Optional.empty()); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.updateZoo(zooId, updatedZoo) + ); + assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); +} +``` + +#### 3. Delete Zoo When Exists +```java +@Test +@DisplayName("Should delete zoo when exists") +void shouldDeleteZooWhenExists() { + // Given + Long zooId = 1L; + when(zooRepository.existsById(zooId)).thenReturn(true); + + // When + zooService.deleteZoo(zooId); + + // Then + verify(zooRepository, times(1)).deleteById(zooId); +} +``` + +#### 4. Handle Delete Errors +```java +@Test +@DisplayName("Should throw exception when deleting non-existent zoo") +void shouldThrowExceptionWhenDeletingNonExistentZoo() { + // Given + Long zooId = 999L; + when(zooRepository.existsById(zooId)).thenReturn(false); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.deleteZoo(zooId) + ); + assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); +} +``` + +### Controller Layer Solutions + +#### 1. Update Zoo Via REST +```java +@Test +@DisplayName("Should update zoo when exists") +void shouldUpdateZooWhenExists() throws Exception { + // Given + manilaZoo.setId(1L); + Zoo updatedZoo = new Zoo("Updated Manila Zoo", "Updated Location", "Updated description"); + updatedZoo.setId(1L); + + when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); + + // When & Then + mockMvc.perform(put("/api/zoos/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedZoo))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Updated Manila Zoo")); +} +``` + +#### 2. Handle Update 404 +```java +@Test +@DisplayName("Should return 404 when updating non-existent zoo") +void shouldReturn404WhenUpdatingNonExistentZoo() throws Exception { + // Given + Zoo updatedZoo = new Zoo("Updated Zoo", "Updated Location", "Updated description"); + + when(zooService.updateZoo(eq(999L), any(Zoo.class))) + .thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); + + // When & Then + mockMvc.perform(put("/api/zoos/999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedZoo))) + .andExpect(status().isNotFound()); +} +``` + +#### 3. Delete Zoo Via REST +```java +@Test +@DisplayName("Should delete zoo when exists") +void shouldDeleteZooWhenExists() throws Exception { + // Given + doNothing().when(zooService).deleteZoo(1L); + + // When & Then + mockMvc.perform(delete("/api/zoos/1")) + .andExpect(status().isNoContent()); +} +``` + +#### 4. Handle Delete 404 +```java +@Test +@DisplayName("Should return 404 when deleting non-existent zoo") +void shouldReturn404WhenDeletingNonExistentZoo() throws Exception { + // Given + doThrow(new IllegalArgumentException("Zoo not found with id: 999")) + .when(zooService).deleteZoo(999L); + + // When & Then + mockMvc.perform(delete("/api/zoos/999")) + .andExpect(status().isNotFound()); +} +``` + +## Key Learning Points + +### JPA Concepts +- **Entity Relationships**: One-to-Many, Many-to-One mappings +- **ID Generation**: Auto-increment primary keys +- **Column Constraints**: Not null, unique constraints +- **Validation**: Bean validation annotations + +### Testing Strategies +- **@DataJpaTest**: Repository testing with in-memory database +- **@WebMvcTest**: Controller testing with mocked service +- **Mockito**: Service layer testing with mocked repository +- **TestEntityManager**: Direct database operations in tests + +### Database Operations +- **CRUD Operations**: Create, Read, Update, Delete +- **Custom Queries**: Method name-based queries +- **Transaction Management**: Automatic transaction handling +- **Error Handling**: Exception handling for database operations + +## Common Patterns + +### Repository Pattern +```java +public interface ZooRepository extends JpaRepository { + List findByNameContainingIgnoreCase(String name); + List findByLocationContainingIgnoreCase(String location); +} +``` + +### Service Layer Pattern +```java +@Service +public class ZooService { + public Zoo updateZoo(Long id, Zoo zooDetails) { + Optional optionalZoo = zooRepository.findById(id); + if (optionalZoo.isPresent()) { + Zoo zoo = optionalZoo.get(); + // Update fields + return zooRepository.save(zoo); + } else { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + } +} +``` + +### Controller Error Handling +```java +@PutMapping("/{id}") +public ResponseEntity updateZoo(@PathVariable Long id, @RequestBody Zoo zooDetails) { + try { + Zoo updatedZoo = zooService.updateZoo(id, zooDetails); + return ResponseEntity.ok(updatedZoo); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } +} +``` + +## Next Steps + +After completing Lab 3, students will be ready for: +- **Lab 4**: Complete CRUD for Animals, Enclosures, and People +- **Lab 5**: API security implementation +- **Lab 6**: Advanced testing with real database containers \ No newline at end of file diff --git a/lab3/build.gradle b/lab3/build.gradle new file mode 100644 index 0000000..5770320 --- /dev/null +++ b/lab3/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.tddacademy' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'com.h2database:h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/lab3/gradle/wrapper/gradle-wrapper.properties b/lab3/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a118ea3 --- /dev/null +++ b/lab3/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/lab3/gradlew b/lab3/gradlew new file mode 100755 index 0000000..4d629e2 --- /dev/null +++ b/lab3/gradlew @@ -0,0 +1,242 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Gradle template within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# * treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments). +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/ZooApplication.java b/lab3/src/main/java/com/tddacademy/zoo/ZooApplication.java new file mode 100644 index 0000000..7ebce57 --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/ZooApplication.java @@ -0,0 +1,13 @@ +package com.tddacademy.zoo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ZooApplication { + + public static void main(String[] args) { + SpringApplication.run(ZooApplication.class, args); + } + +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/controller/ZooController.java b/lab3/src/main/java/com/tddacademy/zoo/controller/ZooController.java new file mode 100644 index 0000000..732bb4d --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/controller/ZooController.java @@ -0,0 +1,76 @@ +package com.tddacademy.zoo.controller; + +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.service.ZooService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/zoos") +public class ZooController { + + private final ZooService zooService; + + @Autowired + public ZooController(ZooService zooService) { + this.zooService = zooService; + } + + @GetMapping + public ResponseEntity> getAllZoos() { + List zoos = zooService.getAllZoos(); + return ResponseEntity.ok(zoos); + } + + @GetMapping("/{id}") + public ResponseEntity getZooById(@PathVariable Long id) { + Optional zoo = zooService.getZooById(id); + if (zoo.isPresent()) { + return ResponseEntity.ok(zoo.get()); + } else { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping + public ResponseEntity createZoo(@RequestBody Zoo zoo) { + Zoo createdZoo = zooService.createZoo(zoo); + return ResponseEntity.ok(createdZoo); + } + + @PutMapping("/{id}") + public ResponseEntity updateZoo(@PathVariable Long id, @RequestBody Zoo zooDetails) { + try { + Zoo updatedZoo = zooService.updateZoo(id, zooDetails); + return ResponseEntity.ok(updatedZoo); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteZoo(@PathVariable Long id) { + try { + zooService.deleteZoo(id); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/search/name") + public ResponseEntity> searchZoosByName(@RequestParam String name) { + List zoos = zooService.findZoosByName(name); + return ResponseEntity.ok(zoos); + } + + @GetMapping("/search/location") + public ResponseEntity> searchZoosByLocation(@RequestParam String location) { + List zoos = zooService.findZoosByLocation(location); + return ResponseEntity.ok(zoos); + } +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/model/Animal.java b/lab3/src/main/java/com/tddacademy/zoo/model/Animal.java new file mode 100644 index 0000000..816da73 --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/model/Animal.java @@ -0,0 +1,118 @@ +package com.tddacademy.zoo.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +import java.time.LocalDate; + +@Entity +@Table(name = "animals") +public class Animal { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Animal name cannot be null or empty") + @Column(nullable = false) + private String name; + + @NotBlank(message = "Animal species cannot be null or empty") + @Column(nullable = false) + private String species; + + @Column + private String breed; + + @Column(name = "date_of_birth") + private LocalDate dateOfBirth; + + @Positive(message = "Animal weight must be positive") + @Column + private Double weight; + + @Column(name = "health_status") + private String healthStatus; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "enclosure_id") + private Enclosure enclosure; + + // Default constructor for JPA + protected Animal() {} + + public Animal(String name, String species, String breed, LocalDate dateOfBirth, Double weight, String healthStatus) { + this.name = name; + this.species = species; + this.breed = breed; + this.dateOfBirth = dateOfBirth; + this.weight = weight; + this.healthStatus = healthStatus; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSpecies() { + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public LocalDate getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } + + public Double getWeight() { + return weight; + } + + public void setWeight(Double weight) { + this.weight = weight; + } + + public String getHealthStatus() { + return healthStatus; + } + + public void setHealthStatus(String healthStatus) { + this.healthStatus = healthStatus; + } + + public Enclosure getEnclosure() { + return enclosure; + } + + public void setEnclosure(Enclosure enclosure) { + this.enclosure = enclosure; + } +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/model/Enclosure.java b/lab3/src/main/java/com/tddacademy/zoo/model/Enclosure.java new file mode 100644 index 0000000..e3a78b7 --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/model/Enclosure.java @@ -0,0 +1,112 @@ +package com.tddacademy.zoo.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "enclosures") +public class Enclosure { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Enclosure name cannot be null or empty") + @Column(nullable = false) + private String name; + + @NotBlank(message = "Enclosure type cannot be null or empty") + @Column(nullable = false) + private String type; + + @Positive(message = "Enclosure area must be positive") + @Column + private Double area; + + @Column + private String climate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "zoo_id") + private Zoo zoo; + + @OneToMany(mappedBy = "enclosure", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List animals = new ArrayList<>(); + + // Default constructor for JPA + protected Enclosure() {} + + public Enclosure(String name, String type, Double area, String climate) { + this.name = name; + this.type = type; + this.area = area; + this.climate = climate; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Double getArea() { + return area; + } + + public void setArea(Double area) { + this.area = area; + } + + public String getClimate() { + return climate; + } + + public void setClimate(String climate) { + this.climate = climate; + } + + public Zoo getZoo() { + return zoo; + } + + public void setZoo(Zoo zoo) { + this.zoo = zoo; + } + + public List getAnimals() { + return animals; + } + + public void setAnimals(List animals) { + this.animals = animals; + } + + // Helper methods + public void addAnimal(Animal animal) { + animals.add(animal); + animal.setEnclosure(this); + } +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/model/Person.java b/lab3/src/main/java/com/tddacademy/zoo/model/Person.java new file mode 100644 index 0000000..a449296 --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/model/Person.java @@ -0,0 +1,120 @@ +package com.tddacademy.zoo.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +import java.time.LocalDate; + +@Entity +@Table(name = "people") +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Person first name cannot be null or empty") + @Column(name = "first_name", nullable = false) + private String firstName; + + @NotBlank(message = "Person last name cannot be null or empty") + @Column(name = "last_name", nullable = false) + private String lastName; + + @NotBlank(message = "Person role cannot be null or empty") + @Column(nullable = false) + private String role; + + @NotBlank(message = "Person email cannot be null or empty") + @Column(nullable = false, unique = true) + private String email; + + @Column(name = "hire_date") + private LocalDate hireDate; + + @Positive(message = "Person salary must be positive") + @Column + private Double salary; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "zoo_id") + private Zoo zoo; + + // Default constructor for JPA + protected Person() {} + + public Person(String firstName, String lastName, String role, String email, LocalDate hireDate, Double salary) { + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + this.email = email; + this.hireDate = hireDate; + this.salary = salary; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public LocalDate getHireDate() { + return hireDate; + } + + public void setHireDate(LocalDate hireDate) { + this.hireDate = hireDate; + } + + public Double getSalary() { + return salary; + } + + public void setSalary(Double salary) { + this.salary = salary; + } + + public Zoo getZoo() { + return zoo; + } + + public void setZoo(Zoo zoo) { + this.zoo = zoo; + } +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/model/Zoo.java b/lab3/src/main/java/com/tddacademy/zoo/model/Zoo.java new file mode 100644 index 0000000..58d67f6 --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/model/Zoo.java @@ -0,0 +1,103 @@ +package com.tddacademy.zoo.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "zoos") +public class Zoo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Zoo name cannot be null or empty") + @Column(nullable = false) + private String name; + + @NotBlank(message = "Zoo location cannot be null or empty") + @Column(nullable = false) + private String location; + + @Column(columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "zoo", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List enclosures = new ArrayList<>(); + + @OneToMany(mappedBy = "zoo", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List people = new ArrayList<>(); + + // Default constructor for JPA + protected Zoo() {} + + public Zoo(String name, String location, String description) { + this.name = name; + this.location = location; + this.description = description; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getEnclosures() { + return enclosures; + } + + public void setEnclosures(List enclosures) { + this.enclosures = enclosures; + } + + public List getPeople() { + return people; + } + + public void setPeople(List people) { + this.people = people; + } + + // Helper methods + public void addEnclosure(Enclosure enclosure) { + enclosures.add(enclosure); + enclosure.setZoo(this); + } + + public void addPerson(Person person) { + people.add(person); + person.setZoo(this); + } +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/repository/ZooRepository.java b/lab3/src/main/java/com/tddacademy/zoo/repository/ZooRepository.java new file mode 100644 index 0000000..0999e2a --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/repository/ZooRepository.java @@ -0,0 +1,20 @@ +package com.tddacademy.zoo.repository; + +import com.tddacademy.zoo.model.Zoo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ZooRepository extends JpaRepository { + + List findByNameContainingIgnoreCase(String name); + + List findByLocationContainingIgnoreCase(String location); + + Optional findByNameAndLocation(String name, String location); + + boolean existsByNameAndLocation(String name, String location); +} \ No newline at end of file diff --git a/lab3/src/main/java/com/tddacademy/zoo/service/ZooService.java b/lab3/src/main/java/com/tddacademy/zoo/service/ZooService.java new file mode 100644 index 0000000..4d3ce14 --- /dev/null +++ b/lab3/src/main/java/com/tddacademy/zoo/service/ZooService.java @@ -0,0 +1,65 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.repository.ZooRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class ZooService { + + private final ZooRepository zooRepository; + + @Autowired + public ZooService(ZooRepository zooRepository) { + this.zooRepository = zooRepository; + } + + public List getAllZoos() { + return zooRepository.findAll(); + } + + public Optional getZooById(Long id) { + return zooRepository.findById(id); + } + + public Zoo createZoo(Zoo zoo) { + return zooRepository.save(zoo); + } + + public Zoo updateZoo(Long id, Zoo zooDetails) { + Optional optionalZoo = zooRepository.findById(id); + if (optionalZoo.isPresent()) { + Zoo zoo = optionalZoo.get(); + zoo.setName(zooDetails.getName()); + zoo.setLocation(zooDetails.getLocation()); + zoo.setDescription(zooDetails.getDescription()); + return zooRepository.save(zoo); + } else { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + } + + public void deleteZoo(Long id) { + if (zooRepository.existsById(id)) { + zooRepository.deleteById(id); + } else { + throw new IllegalArgumentException("Zoo not found with id: " + id); + } + } + + public List findZoosByName(String name) { + return zooRepository.findByNameContainingIgnoreCase(name); + } + + public List findZoosByLocation(String location) { + return zooRepository.findByLocationContainingIgnoreCase(location); + } + + public boolean zooExists(Long id) { + return zooRepository.existsById(id); + } +} \ No newline at end of file diff --git a/lab3/src/main/resources/application.properties b/lab3/src/main/resources/application.properties new file mode 100644 index 0000000..a4bf455 --- /dev/null +++ b/lab3/src/main/resources/application.properties @@ -0,0 +1,31 @@ +# Server Configuration +server.port=8080 + +# Application Name +spring.application.name=zoo-simulator-lab3 + +# H2 Database Configuration +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +# H2 Console (for development) +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# JPA Configuration +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# Logging Configuration +logging.level.com.tddacademy.zoo=INFO +logging.level.org.springframework.web=INFO +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + +# Jackson Configuration +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.write-dates-as-timestamps=false \ No newline at end of file diff --git a/lab3/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/lab3/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java new file mode 100644 index 0000000..0cd018a --- /dev/null +++ b/lab3/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java @@ -0,0 +1,14 @@ +package com.tddacademy.zoo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZooApplicationTests { + + @Test + void contextLoads() { + // This test verifies that the Spring application context loads successfully + } + +} \ No newline at end of file diff --git a/lab3/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java b/lab3/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java new file mode 100644 index 0000000..f4a417e --- /dev/null +++ b/lab3/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java @@ -0,0 +1,208 @@ +package com.tddacademy.zoo.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.service.ZooService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ZooController.class) +class ZooControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ZooService zooService; + + @Autowired + private ObjectMapper objectMapper; + + private Zoo manilaZoo; + private Zoo cebuSafari; + + @BeforeEach + void setUp() { + manilaZoo = new Zoo("Manila Zoo", "Manila, Philippines", "A beautiful zoo in the heart of Manila"); + cebuSafari = new Zoo("Cebu Safari", "Cebu, Philippines", "World famous safari park"); + } + + @Test + @DisplayName("Should get all zoos") + void shouldGetAllZoos() throws Exception { + // Given + List zoos = Arrays.asList(manilaZoo, cebuSafari); + when(zooService.getAllZoos()).thenReturn(zoos); + + // When & Then + mockMvc.perform(get("/api/zoos")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].name").value("Manila Zoo")) + .andExpect(jsonPath("$[1].name").value("Cebu Safari")); + } + + @Test + @DisplayName("Should get zoo by id when exists") + void shouldGetZooByIdWhenExists() throws Exception { + // Given + manilaZoo.setId(1L); + when(zooService.getZooById(1L)).thenReturn(Optional.of(manilaZoo)); + + // When & Then + mockMvc.perform(get("/api/zoos/1")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")); + } + + @Test + @DisplayName("Should return 404 when zoo not found") + void shouldReturn404WhenZooNotFound() throws Exception { + // Given + when(zooService.getZooById(999L)).thenReturn(Optional.empty()); + + // When & Then + mockMvc.perform(get("/api/zoos/999")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Should create zoo successfully") + void shouldCreateZooSuccessfully() throws Exception { + // Given + manilaZoo.setId(1L); + when(zooService.createZoo(any(Zoo.class))).thenReturn(manilaZoo); + + // When & Then + mockMvc.perform(post("/api/zoos") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(manilaZoo))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")); + } + + @Test + @DisplayName("Should update zoo when exists") + void shouldUpdateZooWhenExists() throws Exception { + // TODO: Complete this test + // 1. Set up the existing zoo with ID 1 + // 2. Create a zoo with updated details + // 3. Mock zooService.updateZoo(1L, any(Zoo.class)) to return the updated zoo + // 4. Perform PUT request to "/api/zoos/1" with the updated zoo JSON + // 5. Expect status 200 (OK) + // 6. Expect the response to contain the updated name + + // Your code here: + // manilaZoo.setId(1L); + // Zoo updatedZoo = new Zoo("Updated Manila Zoo", "Updated Location", "Updated description"); + // updatedZoo.setId(1L); + // + // when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); + // + // mockMvc.perform(put("/api/zoos/1") + // .contentType(MediaType.APPLICATION_JSON) + // .content(objectMapper.writeValueAsString(updatedZoo))) + // .andExpect(status().isOk()) + // .andExpect(jsonPath("$.name").value("Updated Manila Zoo")); + } + + @Test + @DisplayName("Should return 404 when updating non-existent zoo") + void shouldReturn404WhenUpdatingNonExistentZoo() throws Exception { + // TODO: Complete this test + // 1. Create a zoo with updated details + // 2. Mock zooService.updateZoo(999L, any(Zoo.class)) to throw IllegalArgumentException + // 3. Perform PUT request to "/api/zoos/999" with the updated zoo JSON + // 4. Expect status 404 (Not Found) + + // Your code here: + // Zoo updatedZoo = new Zoo("Updated Zoo", "Updated Location", "Updated description"); + // + // when(zooService.updateZoo(eq(999L), any(Zoo.class))) + // .thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); + // + // mockMvc.perform(put("/api/zoos/999") + // .contentType(MediaType.APPLICATION_JSON) + // .content(objectMapper.writeValueAsString(updatedZoo))) + // .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Should delete zoo when exists") + void shouldDeleteZooWhenExists() throws Exception { + // TODO: Complete this test + // 1. Mock zooService.deleteZoo(1L) to do nothing (void method) + // 2. Perform DELETE request to "/api/zoos/1" + // 3. Expect status 204 (No Content) + + // Your code here: + // doNothing().when(zooService).deleteZoo(1L); + // + // mockMvc.perform(delete("/api/zoos/1")) + // .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("Should return 404 when deleting non-existent zoo") + void shouldReturn404WhenDeletingNonExistentZoo() throws Exception { + // TODO: Complete this test + // 1. Mock zooService.deleteZoo(999L) to throw IllegalArgumentException + // 2. Perform DELETE request to "/api/zoos/999" + // 3. Expect status 404 (Not Found) + + // Your code here: + // doThrow(new IllegalArgumentException("Zoo not found with id: 999")) + // .when(zooService).deleteZoo(999L); + // + // mockMvc.perform(delete("/api/zoos/999")) + // .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Should search zoos by name") + void shouldSearchZoosByName() throws Exception { + // Given + List zoos = Arrays.asList(manilaZoo); + when(zooService.findZoosByName("Manila")).thenReturn(zoos); + + // When & Then + mockMvc.perform(get("/api/zoos/search/name") + .param("name", "Manila")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("Manila Zoo")); + } + + @Test + @DisplayName("Should search zoos by location") + void shouldSearchZoosByLocation() throws Exception { + // Given + List zoos = Arrays.asList(manilaZoo, cebuSafari); + when(zooService.findZoosByLocation("Philippines")).thenReturn(zoos); + + // When & Then + mockMvc.perform(get("/api/zoos/search/location") + .param("location", "Philippines")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("Manila Zoo")) + .andExpect(jsonPath("$[1].name").value("Cebu Safari")); + } +} \ No newline at end of file diff --git a/lab3/src/test/java/com/tddacademy/zoo/repository/ZooRepositoryTest.java b/lab3/src/test/java/com/tddacademy/zoo/repository/ZooRepositoryTest.java new file mode 100644 index 0000000..9916566 --- /dev/null +++ b/lab3/src/test/java/com/tddacademy/zoo/repository/ZooRepositoryTest.java @@ -0,0 +1,155 @@ +package com.tddacademy.zoo.repository; + +import com.tddacademy.zoo.model.Zoo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class ZooRepositoryTest { + + @Autowired + private ZooRepository zooRepository; + + @Autowired + private TestEntityManager entityManager; + + private Zoo manilaZoo; + private Zoo cebuSafari; + + @BeforeEach + void setUp() { + // Create test data + manilaZoo = new Zoo("Manila Zoo", "Manila, Philippines", "A beautiful zoo in the heart of Manila"); + cebuSafari = new Zoo("Cebu Safari", "Cebu, Philippines", "World famous safari park"); + } + + @Test + @DisplayName("Should save a zoo successfully") + void shouldSaveZooSuccessfully() { + // When + Zoo savedZoo = zooRepository.save(manilaZoo); + + // Then + assertNotNull(savedZoo.getId()); + assertEquals("Manila Zoo", savedZoo.getName()); + assertEquals("Manila, Philippines", savedZoo.getLocation()); + } + + @Test + @DisplayName("Should find zoo by id") + void shouldFindZooById() { + // Given + Zoo savedZoo = zooRepository.save(manilaZoo); + Long zooId = savedZoo.getId(); + + // When + Optional foundZoo = zooRepository.findById(zooId); + + // Then + assertTrue(foundZoo.isPresent()); + assertEquals("Manila Zoo", foundZoo.get().getName()); + } + + @Test + @DisplayName("Should find all zoos") + void shouldFindAllZoos() { + // Given + zooRepository.save(manilaZoo); + zooRepository.save(cebuSafari); + + // When + List allZoos = zooRepository.findAll(); + + // Then + assertEquals(2, allZoos.size()); + assertTrue(allZoos.stream().anyMatch(zoo -> zoo.getName().equals("Manila Zoo"))); + assertTrue(allZoos.stream().anyMatch(zoo -> zoo.getName().equals("Cebu Safari"))); + } + + @Test + @DisplayName("Should find zoos by name containing") + void shouldFindZoosByNameContaining() { + // TODO: Complete this test + // 1. Save both manilaZoo and cebuSafari to the repository + // 2. Use zooRepository.findByNameContainingIgnoreCase("Manila") to search + // 3. Assert that the result contains only the Manila Zoo + // 4. Assert that the result size is 1 + + // Your code here: + // zooRepository.save(manilaZoo); + // zooRepository.save(cebuSafari); + // + // List manilaZoos = zooRepository.findByNameContainingIgnoreCase("Manila"); + // + // assertEquals(1, manilaZoos.size()); + // assertEquals("Manila Zoo", manilaZoos.get(0).getName()); + } + + @Test + @DisplayName("Should find zoos by location containing") + void shouldFindZoosByLocationContaining() { + // TODO: Complete this test + // 1. Save both manilaZoo and cebuSafari to the repository + // 2. Use zooRepository.findByLocationContainingIgnoreCase("Philippines") to search + // 3. Assert that the result contains both zoos + // 4. Assert that the result size is 2 + + // Your code here: + // zooRepository.save(manilaZoo); + // zooRepository.save(cebuSafari); + // + // List philippineZoos = zooRepository.findByLocationContainingIgnoreCase("Philippines"); + // + // assertEquals(2, philippineZoos.size()); + // assertTrue(philippineZoos.stream().anyMatch(zoo -> zoo.getName().equals("Manila Zoo"))); + // assertTrue(philippineZoos.stream().anyMatch(zoo -> zoo.getName().equals("Cebu Safari"))); + } + + @Test + @DisplayName("Should check if zoo exists by id") + void shouldCheckIfZooExistsById() { + // TODO: Complete this test + // 1. Save manilaZoo to the repository + // 2. Get the saved zoo's ID + // 3. Use zooRepository.existsById(savedId) to check existence + // 4. Assert that the zoo exists + // 5. Use zooRepository.existsById(999L) to check non-existence + // 6. Assert that the zoo does not exist + + // Your code here: + // Zoo savedZoo = zooRepository.save(manilaZoo); + // Long savedId = savedZoo.getId(); + // + // assertTrue(zooRepository.existsById(savedId)); + // assertFalse(zooRepository.existsById(999L)); + } + + @Test + @DisplayName("Should delete zoo by id") + void shouldDeleteZooById() { + // TODO: Complete this test + // 1. Save manilaZoo to the repository + // 2. Get the saved zoo's ID + // 3. Use zooRepository.deleteById(savedId) to delete the zoo + // 4. Use zooRepository.findById(savedId) to try to find the deleted zoo + // 5. Assert that the zoo is not found (Optional.empty()) + + // Your code here: + // Zoo savedZoo = zooRepository.save(manilaZoo); + // Long savedId = savedZoo.getId(); + // + // zooRepository.deleteById(savedId); + // Optional deletedZoo = zooRepository.findById(savedId); + // + // assertTrue(deletedZoo.isEmpty()); + } +} \ No newline at end of file diff --git a/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java b/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java new file mode 100644 index 0000000..590654f --- /dev/null +++ b/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java @@ -0,0 +1,212 @@ +package com.tddacademy.zoo.service; + +import com.tddacademy.zoo.model.Zoo; +import com.tddacademy.zoo.repository.ZooRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ZooServiceTest { + + @Mock + private ZooRepository zooRepository; + + @InjectMocks + private ZooService zooService; + + private Zoo manilaZoo; + private Zoo cebuSafari; + + @BeforeEach + void setUp() { + manilaZoo = new Zoo("Manila Zoo", "Manila, Philippines", "A beautiful zoo in the heart of Manila"); + cebuSafari = new Zoo("Cebu Safari", "Cebu, Philippines", "World famous safari park"); + } + + @Test + @DisplayName("Should get all zoos") + void shouldGetAllZoos() { + // Given + List expectedZoos = Arrays.asList(manilaZoo, cebuSafari); + when(zooRepository.findAll()).thenReturn(expectedZoos); + + // When + List actualZoos = zooService.getAllZoos(); + + // Then + assertEquals(2, actualZoos.size()); + assertEquals("Manila Zoo", actualZoos.get(0).getName()); + assertEquals("Cebu Safari", actualZoos.get(1).getName()); + } + + @Test + @DisplayName("Should get zoo by id when exists") + void shouldGetZooByIdWhenExists() { + // Given + Long zooId = 1L; + manilaZoo.setId(zooId); + when(zooRepository.findById(zooId)).thenReturn(Optional.of(manilaZoo)); + + // When + Optional foundZoo = zooService.getZooById(zooId); + + // Then + assertTrue(foundZoo.isPresent()); + assertEquals("Manila Zoo", foundZoo.get().getName()); + } + + @Test + @DisplayName("Should return empty when zoo not found") + void shouldReturnEmptyWhenZooNotFound() { + // Given + Long zooId = 999L; + when(zooRepository.findById(zooId)).thenReturn(Optional.empty()); + + // When + Optional foundZoo = zooService.getZooById(zooId); + + // Then + assertTrue(foundZoo.isEmpty()); + } + + @Test + @DisplayName("Should create zoo successfully") + void shouldCreateZooSuccessfully() { + // Given + manilaZoo.setId(1L); + when(zooRepository.save(any(Zoo.class))).thenReturn(manilaZoo); + + // When + Zoo createdZoo = zooService.createZoo(manilaZoo); + + // Then + assertNotNull(createdZoo.getId()); + assertEquals("Manila Zoo", createdZoo.getName()); + verify(zooRepository, times(1)).save(manilaZoo); + } + + @Test + @DisplayName("Should update zoo when exists") + void shouldUpdateZooWhenExists() { + // TODO: Complete this test + // 1. Set up the existing zoo with ID 1 + // 2. Create a new zoo with updated details + // 3. Mock zooRepository.findById(1L) to return the existing zoo + // 4. Mock zooRepository.save(any(Zoo.class)) to return the updated zoo + // 5. Call zooService.updateZoo(1L, updatedZoo) + // 6. Assert that the returned zoo has the updated name + // 7. Verify that zooRepository.save was called once + + // Your code here: + // Long zooId = 1L; + // manilaZoo.setId(zooId); + // Zoo updatedZoo = new Zoo("Updated Manila Zoo", "Updated Location", "Updated description"); + // + // when(zooRepository.findById(zooId)).thenReturn(Optional.of(manilaZoo)); + // when(zooRepository.save(any(Zoo.class))).thenReturn(updatedZoo); + // + // Zoo result = zooService.updateZoo(zooId, updatedZoo); + // + // assertEquals("Updated Manila Zoo", result.getName()); + // verify(zooRepository, times(1)).save(any(Zoo.class)); + } + + @Test + @DisplayName("Should throw exception when updating non-existent zoo") + void shouldThrowExceptionWhenUpdatingNonExistentZoo() { + // TODO: Complete this test + // 1. Mock zooRepository.findById(999L) to return Optional.empty() + // 2. Create a zoo with updated details + // 3. Use assertThrows to test that zooService.updateZoo(999L, updatedZoo) throws IllegalArgumentException + // 4. Verify the exception message contains "Zoo not found with id: 999" + + // Your code here: + // Long zooId = 999L; + // Zoo updatedZoo = new Zoo("Updated Zoo", "Updated Location", "Updated description"); + // + // when(zooRepository.findById(zooId)).thenReturn(Optional.empty()); + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> zooService.updateZoo(zooId, updatedZoo) + // ); + // assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); + } + + @Test + @DisplayName("Should delete zoo when exists") + void shouldDeleteZooWhenExists() { + // TODO: Complete this test + // 1. Mock zooRepository.existsById(1L) to return true + // 2. Call zooService.deleteZoo(1L) + // 3. Verify that zooRepository.deleteById(1L) was called once + + // Your code here: + // Long zooId = 1L; + // when(zooRepository.existsById(zooId)).thenReturn(true); + // + // zooService.deleteZoo(zooId); + // + // verify(zooRepository, times(1)).deleteById(zooId); + } + + @Test + @DisplayName("Should throw exception when deleting non-existent zoo") + void shouldThrowExceptionWhenDeletingNonExistentZoo() { + // TODO: Complete this test + // 1. Mock zooRepository.existsById(999L) to return false + // 2. Use assertThrows to test that zooService.deleteZoo(999L) throws IllegalArgumentException + // 3. Verify the exception message contains "Zoo not found with id: 999" + + // Your code here: + // Long zooId = 999L; + // when(zooRepository.existsById(zooId)).thenReturn(false); + // + // IllegalArgumentException exception = assertThrows( + // IllegalArgumentException.class, + // () -> zooService.deleteZoo(zooId) + // ); + // assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); + } + + @Test + @DisplayName("Should find zoos by name") + void shouldFindZoosByName() { + // Given + List expectedZoos = Arrays.asList(manilaZoo); + when(zooRepository.findByNameContainingIgnoreCase("Manila")).thenReturn(expectedZoos); + + // When + List foundZoos = zooService.findZoosByName("Manila"); + + // Then + assertEquals(1, foundZoos.size()); + assertEquals("Manila Zoo", foundZoos.get(0).getName()); + } + + @Test + @DisplayName("Should check if zoo exists") + void shouldCheckIfZooExists() { + // Given + Long zooId = 1L; + when(zooRepository.existsById(zooId)).thenReturn(true); + when(zooRepository.existsById(999L)).thenReturn(false); + + // When & Then + assertTrue(zooService.zooExists(zooId)); + assertFalse(zooService.zooExists(999L)); + } +} \ No newline at end of file From 44c23e6ffb1b4560734d7b12abe29a6b4e6a27d2 Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Mon, 21 Jul 2025 18:08:58 +0800 Subject: [PATCH 3/8] vincent and joshua final commit --- .../zoo/service/TodoExercisesTest.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java index c13c6de..66f871b 100644 --- a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java @@ -45,6 +45,7 @@ void setUp() { nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); timon = new Animal("Timon", "Meerkat", 2.5, LocalDate.of(2021, 3, 10), "Healthy"); zooManager = new ZooManager(animalService, notificationService); + } // ========== MOCK EXERCISES ========== @@ -88,19 +89,13 @@ void shouldVerifyRepositorySaveWasCalled() { @Test @DisplayName("TODO: Stub Exercise 1 - Should calculate average weight with stub data") void shouldCalculateAverageWeightWithStubData() { - // TODO: Complete this test using stubs - // 1. Create stub data: simba (180.5), nala (160.0), timon (2.5) - // 2. Mock animalRepository.findAll() to return this stub data - // 3. Call animalService.getAverageWeight() - // 4. Assert the average is 114.33 (with 0.01 precision) - - // Your code here: - // List animals = Arrays.asList(simba, nala, timon); - // when(animalRepository.findAll()).thenReturn(animals); - // - // double averageWeight = animalService.getAverageWeight(); - // - // assertEquals(114.33, averageWeight, 0.01); + + List animals = Arrays.asList(simba, nala, timon); + when(animalRepository.findAll()).thenReturn(animals); + + double averageWeight = animalService.getAverageWeight(); + + assertEquals(114.33, averageWeight, 0.01); } @Test @@ -228,4 +223,6 @@ void shouldHandleComplexScenarioWithMultipleMocks() { ); verify(animalRepository, times(1)).findById(1L); } + + } \ No newline at end of file From b071e55d4979b696d829f9d2029158ca7a462989 Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Mon, 21 Jul 2025 19:41:38 +0800 Subject: [PATCH 4/8] vincent and joshua final submittion lab0.5 --- lab0.5/README.md | 268 ------------ lab0.5/SUMMARY.md | 392 ------------------ lab0.5/build.gradle | 29 -- lab0.5/gradlew | 242 ----------- .../java/com/tddacademy/zoo/model/Animal.java | 83 ---- .../zoo/service/AnimalRepository.java | 22 - .../tddacademy/zoo/service/AnimalService.java | 60 --- .../zoo/service/NotificationService.java | 24 -- .../tddacademy/zoo/service/ZooManager.java | 63 --- .../zoo/service/MockExamplesTest.java | 210 ---------- .../zoo/service/SpyExamplesTest.java | 199 --------- .../zoo/service/StubExamplesTest.java | 210 ---------- .../zoo/service/TodoExercisesTest.java | 295 ------------- lab1/gradle/wrapper/gradle-wrapper.properties | 7 - .../com/tddacademy/zoo/ZooApplication.java | 13 - .../tddacademy/zoo/ZooApplicationTests.java | 14 - .../gradle/wrapper/gradle-wrapper.properties | 4 +- session1/lab0.5/gradlew | 33 +- session1/lab0.5/gradlew.bat | 94 ----- .../zoo/service/TodoExercisesTest.java | 102 ++++- {lab1 => session1/lab1}/README.md | 0 {lab1 => session1/lab1}/SUMMARY.md | 0 {lab1 => session1/lab1}/build.gradle | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 {lab1 => session1/lab1}/gradlew | 0 {lab1 => session1/lab1}/gradlew.bat | 0 .../com/tddacademy/zoo/ZooApplication.java | 0 .../java/com/tddacademy/zoo/model/Animal.java | 0 .../com/tddacademy/zoo/model/Enclosure.java | 0 .../java/com/tddacademy/zoo/model/Person.java | 0 .../java/com/tddacademy/zoo/model/Zoo.java | 0 .../src/main/resources/application.properties | 0 .../tddacademy/zoo/ZooApplicationTests.java | 0 .../com/tddacademy/zoo/model/AnimalTest.java | 78 ++-- .../tddacademy/zoo/model/EnclosureTest.java | 25 +- .../com/tddacademy/zoo/model/PersonTest.java | 114 ++--- .../com/tddacademy/zoo/model/ZooTest.java | 0 37 files changed, 208 insertions(+), 2373 deletions(-) delete mode 100644 lab0.5/README.md delete mode 100644 lab0.5/SUMMARY.md delete mode 100644 lab0.5/build.gradle delete mode 100755 lab0.5/gradlew delete mode 100644 lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java delete mode 100644 lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java delete mode 100644 lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java delete mode 100644 lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java delete mode 100644 lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java delete mode 100644 lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java delete mode 100644 lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java delete mode 100644 lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java delete mode 100644 lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java delete mode 100644 lab1/gradle/wrapper/gradle-wrapper.properties delete mode 100644 lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java delete mode 100644 lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java delete mode 100644 session1/lab0.5/gradlew.bat rename {lab1 => session1/lab1}/README.md (100%) rename {lab1 => session1/lab1}/SUMMARY.md (100%) rename {lab1 => session1/lab1}/build.gradle (100%) rename {lab0.5 => session1/lab1}/gradle/wrapper/gradle-wrapper.properties (100%) rename {lab1 => session1/lab1}/gradlew (100%) rename {lab1 => session1/lab1}/gradlew.bat (100%) rename {lab0.5 => session1/lab1}/src/main/java/com/tddacademy/zoo/ZooApplication.java (100%) rename {lab1 => session1/lab1}/src/main/java/com/tddacademy/zoo/model/Animal.java (100%) rename {lab1 => session1/lab1}/src/main/java/com/tddacademy/zoo/model/Enclosure.java (100%) rename {lab1 => session1/lab1}/src/main/java/com/tddacademy/zoo/model/Person.java (100%) rename {lab1 => session1/lab1}/src/main/java/com/tddacademy/zoo/model/Zoo.java (100%) rename {lab1 => session1/lab1}/src/main/resources/application.properties (100%) rename {lab0.5 => session1/lab1}/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java (100%) rename {lab1 => session1/lab1}/src/test/java/com/tddacademy/zoo/model/AnimalTest.java (68%) rename {lab1 => session1/lab1}/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java (89%) rename {lab1 => session1/lab1}/src/test/java/com/tddacademy/zoo/model/PersonTest.java (65%) rename {lab1 => session1/lab1}/src/test/java/com/tddacademy/zoo/model/ZooTest.java (100%) diff --git a/lab0.5/README.md b/lab0.5/README.md deleted file mode 100644 index 7f83373..0000000 --- a/lab0.5/README.md +++ /dev/null @@ -1,268 +0,0 @@ -# Lab 0.5: Test Doubles with Mockito - -## Overview - -Lab 0.5 introduces **Test Doubles** using **Mockito**, a powerful Java testing framework. You'll learn about different types of test doubles: **Mocks**, **Stubs**, and **Spies**, and how to use them effectively in unit testing. - -## Learning Objectives - -By the end of this lab, you will be able to: - -- ✅ **Understand Test Doubles**: Know when and why to use different types -- ✅ **Use Mocks**: Create and configure mocks for behavior verification -- ✅ **Use Stubs**: Provide predefined responses for method calls -- ✅ **Use Spies**: Monitor real object interactions -- ✅ **Write Clean Tests**: Create readable and maintainable test code -- ✅ **Verify Interactions**: Ensure methods are called correctly - -## Prerequisites - -- Java 17 or higher -- Gradle 7.0 or higher -- Basic understanding of JUnit 5 - -## What are Test Doubles? - -Test doubles are objects that replace real dependencies in tests. They help you: - -- **Isolate the unit under test** -- **Control test data and behavior** -- **Verify interactions between components** -- **Speed up test execution** - -## Types of Test Doubles - -### 1. **Mock** 🎭 -- **Purpose**: Verify behavior and interactions -- **Use when**: You want to ensure methods are called correctly -- **Key features**: Behavior verification, interaction tracking - -### 2. **Stub** 📋 -- **Purpose**: Provide predefined responses -- **Use when**: You need specific data for your test -- **Key features**: Return values, no behavior verification - -### 3. **Spy** 👁️ -- **Purpose**: Monitor real object interactions -- **Use when**: You want to track calls to a real object -- **Key features**: Real behavior + verification capabilities - -## Project Structure - -``` -lab0.5/ -├── src/ -│ ├── main/ -│ │ └── java/com/tddacademy/zoo/ -│ │ ├── model/ # Simple Animal model -│ │ └── service/ # Service classes for testing -│ └── test/ -│ └── java/com/tddacademy/zoo/ -│ └── service/ # Test examples and exercises -├── build.gradle -└── README.md -``` - -## Key Mockito Concepts - -### Annotations -- **@Mock**: Creates a mock object -- **@Spy**: Creates a spy object -- **@InjectMocks**: Injects mocks into the class under test -- **@ExtendWith(MockitoExtension.class)**: Enables Mockito support - -### Common Methods -- **when()**: Define mock behavior -- **verify()**: Verify method calls -- **times()**: Specify call count -- **any()**: Match any argument -- **eq()**: Match exact argument - -## Getting Started - -### 1. Run Tests -```bash -./gradlew test -``` - -### 2. Run Specific Test Class -```bash -./gradlew test --tests MockExamplesTest -``` - -### 3. Run TODO Exercises -```bash -./gradlew test --tests TodoExercisesTest -``` - -## Test Examples - -### Mock Examples (12 tests) -- Basic mock setup and verification -- Method call verification -- Argument matching -- Return value configuration - -### Stub Examples (12 tests) -- Predefined data responses -- Empty list handling -- Fixed return values -- Edge case scenarios - -### Spy Examples (10 tests) -- Real object monitoring -- Interaction verification -- Parameter validation -- Call count tracking - -## TODO Exercises - -### Mock Exercises (3 exercises) -1. **Find animal by species** - Mock repository method -2. **Handle animal not found** - Mock empty response -3. **Verify repository save** - Verify method calls - -### Stub Exercises (3 exercises) -1. **Calculate average weight** - Use stub data -2. **Handle empty repository** - Stub empty response -3. **Get animal count** - Stub fixed count - -### Spy Exercises (3 exercises) -1. **Verify email notification** - Spy on notification service -2. **Verify SMS notification** - Spy on SMS sending -3. **Verify no notification** - Spy on healthy animal - -### Advanced Exercises (3 exercises) -1. **Multiple repository calls** - Complex verification -2. **Exact parameter matching** - Precise verification -3. **Complex scenarios** - Multiple mocks and spies - -## Sample Code - -### Basic Mock Setup -```java -@Mock -private AnimalRepository animalRepository; - -@InjectMocks -private AnimalService animalService; - -@Test -void shouldCreateAnimal() { - // Given - Animal animal = new Animal("Simba", "Lion", 180.5, ...); - when(animalRepository.save(any(Animal.class))).thenReturn(animal); - - // When - Animal result = animalService.createAnimal(animal); - - // Then - assertNotNull(result); - verify(animalRepository, times(1)).save(animal); -} -``` - -### Stub with Predefined Data -```java -@Test -void shouldCalculateAverageWeight() { - // Given - List animals = Arrays.asList(simba, nala); - when(animalRepository.findAll()).thenReturn(animals); - - // When - double average = animalService.getAverageWeight(); - - // Then - assertEquals(170.25, average, 0.01); -} -``` - -### Spy for Interaction Verification -```java -@Spy -private NotificationService notificationService; - -@Test -void shouldSendNotification() { - // Given - Animal animal = new Animal("Simba", ...); - - // When - zooManager.addNewAnimal(animal); - - // Then - verify(notificationService).sendEmail( - eq("staff@zoo.com"), - eq("New Animal Added"), - contains("Simba") - ); -} -``` - -## Common Patterns - -### When to Use Each Type - -**Use Mocks when:** -- You need to verify method calls -- You want to ensure interactions happen correctly -- You're testing behavior, not data - -**Use Stubs when:** -- You need specific return values -- You want to test different scenarios -- You're testing data flow - -**Use Spies when:** -- You want to monitor real object behavior -- You need both real behavior and verification -- You're testing integration points - -### Best Practices - -1. **Keep tests simple** - One concept per test -2. **Use descriptive names** - Make test purpose clear -3. **Follow AAA pattern** - Arrange, Act, Assert -4. **Verify only what matters** - Don't over-verify -5. **Use meaningful data** - Make test data realistic - -## Exercise Solutions - -All TODO exercises include detailed comments with step-by-step instructions. Solutions are provided in the test file comments to help you learn the patterns. - -## Next Steps - -After completing Lab 0.5, you'll be ready for: -- **Lab 1**: Basic unit testing with JUnit 5 -- **Lab 1.5**: MockMvc testing basics -- **Lab 2**: Controller testing with MockMvc -- **Lab 3**: JPA persistence testing - -## Troubleshooting - -### Common Issues -1. **Tests not running**: Check @ExtendWith(MockitoExtension.class) -2. **Mocks not working**: Ensure @Mock and @InjectMocks are used correctly -3. **Verification failing**: Check method signatures and arguments - -### Useful Commands -```bash -# Run all tests -./gradlew test - -# Run specific test class -./gradlew test --tests MockExamplesTest - -# Run with debug output -./gradlew test --info - -# Clean and rebuild -./gradlew clean test -``` - -## Resources - -- [Mockito Documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html) -- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) -- [Test Double Patterns](https://martinfowler.com/bliki/TestDouble.html) \ No newline at end of file diff --git a/lab0.5/SUMMARY.md b/lab0.5/SUMMARY.md deleted file mode 100644 index 1f96d12..0000000 --- a/lab0.5/SUMMARY.md +++ /dev/null @@ -1,392 +0,0 @@ -# Lab 0.5 Summary: Test Doubles with Mockito - -## Test Overview - -Lab 0.5 contains **46 tests** across four test classes, with **34 completed examples** and **12 TODO exercises** for students to complete. - -## Test Breakdown - -### MockExamplesTest (12 tests) - ✅ Completed -**Mock Examples - Behavior Verification:** -1. `shouldCreateAnimalSuccessfully()` - Basic mock setup and verification -2. `shouldFindAnimalByIdWhenExists()` - Mock with return value -3. `shouldReturnEmptyWhenAnimalNotFound()` - Mock with empty response -4. `shouldGetAllAnimals()` - Mock with list return -5. `shouldCalculateAverageWeight()` - Mock with calculation -6. `shouldReturnZeroAverageWeightForEmptyList()` - Mock with empty list -7. `shouldDeleteAnimalWhenExists()` - Mock with void method -8. `shouldReturnFalseWhenDeletingNonExistentAnimal()` - Mock with boolean -9. `shouldCheckIfAnimalIsHealthy()` - Mock with health check -10. `shouldReturnFalseForUnhealthyAnimal()` - Mock with sick animal -11. `shouldReturnFalseForNonExistentAnimal()` - Mock with not found -12. `shouldGetAnimalCount()` - Mock with count - -### StubExamplesTest (12 tests) - ✅ Completed -**Stub Examples - Predefined Responses:** -1. `shouldFindAnimalsBySpeciesUsingStub()` - Stub with species data -2. `shouldReturnEmptyListForNonExistentSpecies()` - Stub with empty list -3. `shouldGetAnimalCountUsingStub()` - Stub with fixed count -4. `shouldCalculateAverageWeightWithStubData()` - Stub with multiple animals -5. `shouldHandleHealthyAnimalCheckWithStub()` - Stub with healthy animal -6. `shouldHandleSickAnimalCheckWithStub()` - Stub with sick animal -7. `shouldCreateAnimalWithStubResponse()` - Stub with saved animal -8. `shouldDeleteAnimalSuccessfullyWithStub()` - Stub with existence check -9. `shouldFailToDeleteNonExistentAnimalWithStub()` - Stub with non-existence -10. `shouldGetAllAnimalsWithStubData()` - Stub with multiple animals -11. `shouldHandleEmptyRepositoryWithStub()` - Stub with empty repository -12. `shouldCalculateZeroAverageForEmptyRepository()` - Stub with zero average - -### SpyExamplesTest (10 tests) - ✅ Completed -**Spy Examples - Real Object Monitoring:** -1. `shouldVerifyNotificationWasSentWhenAddingAnimal()` - Spy on email sending -2. `shouldVerifySMSWasSentWhenRemovingAnimal()` - Spy on SMS sending -3. `shouldVerifyEmailWasSentForUnhealthyAnimal()` - Spy on health alerts -4. `shouldNotSendNotificationForHealthyAnimal()` - Spy on no notification -5. `shouldVerifyNotificationCount()` - Spy on count tracking -6. `shouldVerifyEmailServiceAvailabilityCheck()` - Spy on availability -7. `shouldVerifyMultipleNotificationsForMultipleAnimals()` - Spy on multiple calls -8. `shouldVerifyNotificationParameters()` - Spy on exact parameters -9. `shouldVerifyNoNotificationsForFailedOperations()` - Spy on failure -10. `shouldVerifyNotificationServiceInteraction()` - Spy on interaction - -### TodoExercisesTest (12 tests) - 📝 TODO Exercises -**Student Exercises:** -- **Mock Exercises (3)**: Basic mock usage -- **Stub Exercises (3)**: Predefined data responses -- **Spy Exercises (3)**: Real object monitoring -- **Advanced Exercises (3)**: Complex scenarios - -## TODO Exercise Solutions - -### Mock Exercises - -#### 1. Find Animal by Species -```java -@Test -@DisplayName("TODO: Mock Exercise 1 - Should find animal by species") -void shouldFindAnimalBySpecies() { - // Given - when(animalRepository.findBySpecies("Lion")).thenReturn(Arrays.asList(simba, nala)); - - // When - List lions = animalService.getAnimalsBySpecies("Lion"); - - // Then - assertEquals(2, lions.size()); - assertTrue(lions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); -} -``` - -#### 2. Handle Animal Not Found -```java -@Test -@DisplayName("TODO: Mock Exercise 2 - Should handle animal not found") -void shouldHandleAnimalNotFound() { - // Given - when(animalRepository.findById(999L)).thenReturn(Optional.empty()); - - // When - Optional result = animalService.getAnimalById(999L); - - // Then - assertTrue(result.isEmpty()); -} -``` - -#### 3. Verify Repository Save -```java -@Test -@DisplayName("TODO: Mock Exercise 3 - Should verify repository save was called") -void shouldVerifyRepositorySaveWasCalled() { - // Given - simba.setId(1L); - when(animalRepository.save(any(Animal.class))).thenReturn(simba); - - // When - animalService.createAnimal(simba); - - // Then - verify(animalRepository, times(1)).save(simba); -} -``` - -### Stub Exercises - -#### 1. Calculate Average Weight -```java -@Test -@DisplayName("TODO: Stub Exercise 1 - Should calculate average weight with stub data") -void shouldCalculateAverageWeightWithStubData() { - // Given - List animals = Arrays.asList(simba, nala, timon); - when(animalRepository.findAll()).thenReturn(animals); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - assertEquals(114.33, averageWeight, 0.01); -} -``` - -#### 2. Handle Empty Repository -```java -@Test -@DisplayName("TODO: Stub Exercise 2 - Should handle empty repository with stub") -void shouldHandleEmptyRepositoryWithStub() { - // Given - when(animalRepository.findAll()).thenReturn(Arrays.asList()); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - assertEquals(0.0, averageWeight, 0.01); -} -``` - -#### 3. Get Animal Count -```java -@Test -@DisplayName("TODO: Stub Exercise 3 - Should get animal count with stub") -void shouldGetAnimalCountWithStub() { - // Given - when(animalRepository.count()).thenReturn(15); - - // When - int count = animalService.getAnimalCount(); - - // Then - assertEquals(15, count); -} -``` - -### Spy Exercises - -#### 1. Verify Email Notification -```java -@Test -@DisplayName("TODO: Spy Exercise 1 - Should verify email notification for new animal") -void shouldVerifyEmailNotificationForNewAnimal() { - // Given - simba.setId(1L); - when(animalRepository.save(any(Animal.class))).thenReturn(simba); - - // When - zooManager.addNewAnimal(simba); - - // Then - verify(notificationService, times(1)).sendEmail( - eq("staff@zoo.com"), - eq("New Animal Added"), - contains("Simba") - ); -} -``` - -#### 2. Verify SMS Notification -```java -@Test -@DisplayName("TODO: Spy Exercise 2 - Should verify SMS notification for animal removal") -void shouldVerifySMSNotificationForAnimalRemoval() { - // Given - simba.setId(1L); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - when(animalRepository.existsById(1L)).thenReturn(true); - doNothing().when(animalRepository).deleteById(1L); - - // When - zooManager.removeAnimal(1L); - - // Then - verify(notificationService, times(1)).sendSMS( - eq("+1234567890"), - contains("Simba") - ); -} -``` - -#### 3. Verify No Notification -```java -@Test -@DisplayName("TODO: Spy Exercise 3 - Should verify no notification for healthy animal") -void shouldVerifyNoNotificationForHealthyAnimal() { - // Given - simba.setId(1L); - simba.setHealthStatus("Healthy"); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - zooManager.checkAnimalHealth(1L); - - // Then - verify(notificationService, never()).sendEmail(any(), any(), any()); -} -``` - -### Advanced Exercises - -#### 1. Multiple Repository Calls -```java -@Test -@DisplayName("TODO: Advanced Exercise 1 - Should verify multiple repository calls") -void shouldVerifyMultipleRepositoryCalls() { - // Given - List animals = Arrays.asList(simba, nala); - when(animalRepository.findAll()).thenReturn(animals); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - verify(animalRepository, times(1)).findAll(); - assertEquals(170.25, averageWeight, 0.01); -} -``` - -#### 2. Exact Parameter Matching -```java -@Test -@DisplayName("TODO: Advanced Exercise 2 - Should verify notification parameters exactly") -void shouldVerifyNotificationParametersExactly() { - // Given - simba.setId(1L); - when(animalRepository.save(any(Animal.class))).thenReturn(simba); - - // When - zooManager.addNewAnimal(simba); - - // Then - verify(notificationService).sendEmail( - "staff@zoo.com", - "New Animal Added", - "New animal Simba has been added to the zoo." - ); -} -``` - -#### 3. Complex Scenario -```java -@Test -@DisplayName("TODO: Advanced Exercise 3 - Should handle complex scenario with multiple mocks") -void shouldHandleComplexScenarioWithMultipleMocks() { - // Given - simba.setId(1L); - simba.setHealthStatus("Sick"); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - zooManager.checkAnimalHealth(1L); - - // Then - verify(notificationService, times(1)).sendEmail( - eq("vet@zoo.com"), - eq("Animal Health Alert"), - contains("1") - ); - verify(animalRepository, times(1)).findById(1L); -} -``` - -## Key Learning Points - -### Mock Concepts -- **Behavior Verification**: Ensure methods are called correctly -- **Return Value Configuration**: Control what mocks return -- **Argument Matching**: Use any(), eq(), contains() for flexible matching -- **Call Verification**: Verify method calls with times(), never(), atLeastOnce() - -### Stub Concepts -- **Predefined Data**: Return specific values for testing scenarios -- **Edge Cases**: Handle empty lists, null values, error conditions -- **Data Flow**: Test how data moves through your application -- **Scenario Testing**: Test different business scenarios - -### Spy Concepts -- **Real Object Monitoring**: Track calls to real objects -- **Interaction Verification**: Ensure correct interactions happen -- **Parameter Validation**: Verify exact parameters passed -- **Integration Testing**: Test component interactions - -## Common Patterns - -### Mock Pattern -```java -@Mock -private Dependency dependency; - -@InjectMocks -private ClassUnderTest classUnderTest; - -@Test -void shouldDoSomething() { - // Given - when(dependency.method(any())).thenReturn(expectedValue); - - // When - Result result = classUnderTest.method(); - - // Then - verify(dependency, times(1)).method(any()); - assertEquals(expectedValue, result); -} -``` - -### Stub Pattern -```java -@Test -void shouldHandleScenario() { - // Given - when(repository.findAll()).thenReturn(Arrays.asList(item1, item2)); - - // When - double average = service.calculateAverage(); - - // Then - assertEquals(expectedAverage, average, 0.01); -} -``` - -### Spy Pattern -```java -@Spy -private RealService realService; - -@Test -void shouldInteractCorrectly() { - // Given - Input input = new Input(); - - // When - service.process(input); - - // Then - verify(realService).method(eq(expectedParam)); -} -``` - -## Best Practices - -### When to Use Each Type -- **Mocks**: When you need to verify behavior and interactions -- **Stubs**: When you need predefined responses for testing scenarios -- **Spies**: When you want to monitor real object behavior - -### Test Structure -1. **Arrange**: Set up mocks, stubs, and test data -2. **Act**: Call the method under test -3. **Assert**: Verify results and interactions - -### Verification Guidelines -- Verify only what matters for the test -- Use appropriate matchers (any(), eq(), contains()) -- Check call counts when relevant -- Verify interactions in complex scenarios - -## Next Steps - -After completing Lab 0.5, students will be ready for: -- **Lab 1**: Basic unit testing with JUnit 5 -- **Lab 1.5**: MockMvc testing basics -- **Lab 2**: Controller testing with MockMvc -- **Lab 3**: JPA persistence testing \ No newline at end of file diff --git a/lab0.5/build.gradle b/lab0.5/build.gradle deleted file mode 100644 index 622243e..0000000 --- a/lab0.5/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.5.3' - id 'io.spring.dependency-management' version '1.1.6' -} - -group = 'com.tddacademy' -version = '0.0.1-SNAPSHOT' - -java { - sourceCompatibility = '17' -} - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.mockito:mockito-core' - testImplementation 'org.mockito:mockito-junit-jupiter' -} - -tasks.named('test') { - useJUnitPlatform() -} \ No newline at end of file diff --git a/lab0.5/gradlew b/lab0.5/gradlew deleted file mode 100755 index 4d629e2..0000000 --- a/lab0.5/gradlew +++ /dev/null @@ -1,242 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Gradle template within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# * and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# * treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments). -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java b/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java deleted file mode 100644 index 42bda42..0000000 --- a/lab0.5/src/main/java/com/tddacademy/zoo/model/Animal.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.tddacademy.zoo.model; - -import java.time.LocalDate; - -public class Animal { - private Long id; - private String name; - private String species; - private Double weight; - private LocalDate dateOfBirth; - private String healthStatus; - - public Animal() {} - - public Animal(String name, String species, Double weight, LocalDate dateOfBirth, String healthStatus) { - this.name = name; - this.species = species; - this.weight = weight; - this.dateOfBirth = dateOfBirth; - this.healthStatus = healthStatus; - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getSpecies() { - return species; - } - - public void setSpecies(String species) { - this.species = species; - } - - public Double getWeight() { - return weight; - } - - public void setWeight(Double weight) { - this.weight = weight; - } - - public LocalDate getDateOfBirth() { - return dateOfBirth; - } - - public void setDateOfBirth(LocalDate dateOfBirth) { - this.dateOfBirth = dateOfBirth; - } - - public String getHealthStatus() { - return healthStatus; - } - - public void setHealthStatus(String healthStatus) { - this.healthStatus = healthStatus; - } - - @Override - public String toString() { - return "Animal{" + - "id=" + id + - ", name='" + name + '\'' + - ", species='" + species + '\'' + - ", weight=" + weight + - ", dateOfBirth=" + dateOfBirth + - ", healthStatus='" + healthStatus + '\'' + - '}'; - } -} \ No newline at end of file diff --git a/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java b/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java deleted file mode 100644 index 8fd68ce..0000000 --- a/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import java.util.List; -import java.util.Optional; - -public interface AnimalRepository { - - Animal save(Animal animal); - - Optional findById(Long id); - - List findAll(); - - List findBySpecies(String species); - - void deleteById(Long id); - - boolean existsById(Long id); - - int count(); -} \ No newline at end of file diff --git a/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java b/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java deleted file mode 100644 index 06846b3..0000000 --- a/lab0.5/src/main/java/com/tddacademy/zoo/service/AnimalService.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import java.util.List; -import java.util.Optional; - -public class AnimalService { - - private final AnimalRepository animalRepository; - - public AnimalService(AnimalRepository animalRepository) { - this.animalRepository = animalRepository; - } - - public Animal createAnimal(Animal animal) { - return animalRepository.save(animal); - } - - public Optional getAnimalById(Long id) { - return animalRepository.findById(id); - } - - public List getAllAnimals() { - return animalRepository.findAll(); - } - - public List getAnimalsBySpecies(String species) { - return animalRepository.findBySpecies(species); - } - - public boolean deleteAnimal(Long id) { - if (animalRepository.existsById(id)) { - animalRepository.deleteById(id); - return true; - } - return false; - } - - public int getAnimalCount() { - return animalRepository.count(); - } - - public boolean isAnimalHealthy(Long id) { - Optional animal = animalRepository.findById(id); - return animal.map(a -> "Healthy".equals(a.getHealthStatus())).orElse(false); - } - - public double getAverageWeight() { - List animals = animalRepository.findAll(); - if (animals.isEmpty()) { - return 0.0; - } - - double totalWeight = animals.stream() - .mapToDouble(Animal::getWeight) - .sum(); - - return totalWeight / animals.size(); - } -} \ No newline at end of file diff --git a/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java b/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java deleted file mode 100644 index f82bf68..0000000 --- a/lab0.5/src/main/java/com/tddacademy/zoo/service/NotificationService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tddacademy.zoo.service; - -public class NotificationService { - - public void sendEmail(String to, String subject, String message) { - // This would normally send an email - System.out.println("Email sent to: " + to + " - Subject: " + subject + " - Message: " + message); - } - - public void sendSMS(String phoneNumber, String message) { - // This would normally send an SMS - System.out.println("SMS sent to: " + phoneNumber + " - Message: " + message); - } - - public boolean isEmailServiceAvailable() { - // This would normally check if email service is available - return true; - } - - public int getNotificationCount() { - // This would normally return the count of sent notifications - return 0; - } -} \ No newline at end of file diff --git a/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java b/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java deleted file mode 100644 index a9d7a79..0000000 --- a/lab0.5/src/main/java/com/tddacademy/zoo/service/ZooManager.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import java.util.List; -import java.util.Optional; - -public class ZooManager { - - private final AnimalService animalService; - private final NotificationService notificationService; - - public ZooManager(AnimalService animalService, NotificationService notificationService) { - this.animalService = animalService; - this.notificationService = notificationService; - } - - public Animal addNewAnimal(Animal animal) { - Animal savedAnimal = animalService.createAnimal(animal); - - // Notify staff about new animal - notificationService.sendEmail("staff@zoo.com", - "New Animal Added", - "New animal " + animal.getName() + " has been added to the zoo."); - - return savedAnimal; - } - - public boolean removeAnimal(Long animalId) { - Optional animal = animalService.getAnimalById(animalId); - - if (animal.isPresent()) { - boolean deleted = animalService.deleteAnimal(animalId); - - if (deleted) { - notificationService.sendSMS("+1234567890", - "Animal " + animal.get().getName() + " has been removed from the zoo."); - return true; - } - } - - return false; - } - - public void checkAnimalHealth(Long animalId) { - if (!animalService.isAnimalHealthy(animalId)) { - notificationService.sendEmail("vet@zoo.com", - "Animal Health Alert", - "Animal with ID " + animalId + " needs medical attention."); - } - } - - public int getTotalAnimals() { - return animalService.getAnimalCount(); - } - - public double getAverageAnimalWeight() { - return animalService.getAverageWeight(); - } - - public List getAnimalsBySpecies(String species) { - return animalService.getAnimalsBySpecies(species); - } -} \ No newline at end of file diff --git a/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java b/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java deleted file mode 100644 index 4ec9f54..0000000 --- a/lab0.5/src/test/java/com/tddacademy/zoo/service/MockExamplesTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class MockExamplesTest { - - @Mock - private AnimalRepository animalRepository; - - @InjectMocks - private AnimalService animalService; - - private Animal simba; - private Animal nala; - - @BeforeEach - void setUp() { - simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); - nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); - } - - @Test - @DisplayName("Mock Example 1: Should create animal successfully") - void shouldCreateAnimalSuccessfully() { - // Given - Setup the mock behavior - simba.setId(1L); - when(animalRepository.save(any(Animal.class))).thenReturn(simba); - - // When - Call the method under test - Animal createdAnimal = animalService.createAnimal(simba); - - // Then - Verify the result and mock interaction - assertNotNull(createdAnimal); - assertEquals("Simba", createdAnimal.getName()); - verify(animalRepository, times(1)).save(simba); - } - - @Test - @DisplayName("Mock Example 2: Should find animal by id when exists") - void shouldFindAnimalByIdWhenExists() { - // Given - simba.setId(1L); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - Optional foundAnimal = animalService.getAnimalById(1L); - - // Then - assertTrue(foundAnimal.isPresent()); - assertEquals("Simba", foundAnimal.get().getName()); - } - - @Test - @DisplayName("Mock Example 3: Should return empty when animal not found") - void shouldReturnEmptyWhenAnimalNotFound() { - // Given - when(animalRepository.findById(999L)).thenReturn(Optional.empty()); - - // When - Optional foundAnimal = animalService.getAnimalById(999L); - - // Then - assertTrue(foundAnimal.isEmpty()); - } - - @Test - @DisplayName("Mock Example 4: Should get all animals") - void shouldGetAllAnimals() { - // Given - List animals = Arrays.asList(simba, nala); - when(animalRepository.findAll()).thenReturn(animals); - - // When - List allAnimals = animalService.getAllAnimals(); - - // Then - assertEquals(2, allAnimals.size()); - assertEquals("Simba", allAnimals.get(0).getName()); - assertEquals("Nala", allAnimals.get(1).getName()); - } - - @Test - @DisplayName("Mock Example 5: Should calculate average weight") - void shouldCalculateAverageWeight() { - // Given - List animals = Arrays.asList(simba, nala); - when(animalRepository.findAll()).thenReturn(animals); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - assertEquals(170.25, averageWeight, 0.01); - } - - @Test - @DisplayName("Mock Example 6: Should return zero average weight for empty list") - void shouldReturnZeroAverageWeightForEmptyList() { - // Given - when(animalRepository.findAll()).thenReturn(Arrays.asList()); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - assertEquals(0.0, averageWeight, 0.01); - } - - @Test - @DisplayName("Mock Example 7: Should delete animal when exists") - void shouldDeleteAnimalWhenExists() { - // Given - when(animalRepository.existsById(1L)).thenReturn(true); - doNothing().when(animalRepository).deleteById(1L); - - // When - boolean deleted = animalService.deleteAnimal(1L); - - // Then - assertTrue(deleted); - verify(animalRepository, times(1)).deleteById(1L); - } - - @Test - @DisplayName("Mock Example 8: Should return false when deleting non-existent animal") - void shouldReturnFalseWhenDeletingNonExistentAnimal() { - // Given - when(animalRepository.existsById(999L)).thenReturn(false); - - // When - boolean deleted = animalService.deleteAnimal(999L); - - // Then - assertFalse(deleted); - verify(animalRepository, never()).deleteById(any()); - } - - @Test - @DisplayName("Mock Example 9: Should check if animal is healthy") - void shouldCheckIfAnimalIsHealthy() { - // Given - simba.setId(1L); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - boolean isHealthy = animalService.isAnimalHealthy(1L); - - // Then - assertTrue(isHealthy); - } - - @Test - @DisplayName("Mock Example 10: Should return false for unhealthy animal") - void shouldReturnFalseForUnhealthyAnimal() { - // Given - simba.setId(1L); - simba.setHealthStatus("Sick"); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - boolean isHealthy = animalService.isAnimalHealthy(1L); - - // Then - assertFalse(isHealthy); - } - - @Test - @DisplayName("Mock Example 11: Should return false for non-existent animal") - void shouldReturnFalseForNonExistentAnimal() { - // Given - when(animalRepository.findById(999L)).thenReturn(Optional.empty()); - - // When - boolean isHealthy = animalService.isAnimalHealthy(999L); - - // Then - assertFalse(isHealthy); - } - - @Test - @DisplayName("Mock Example 12: Should get animal count") - void shouldGetAnimalCount() { - // Given - when(animalRepository.count()).thenReturn(5); - - // When - int count = animalService.getAnimalCount(); - - // Then - assertEquals(5, count); - } -} \ No newline at end of file diff --git a/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java b/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java deleted file mode 100644 index a129f99..0000000 --- a/lab0.5/src/test/java/com/tddacademy/zoo/service/SpyExamplesTest.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class SpyExamplesTest { - - @Mock - private AnimalRepository animalRepository; - - @Mock - private AnimalService animalService; - - @Spy - private NotificationService notificationService; - - @InjectMocks - private ZooManager zooManager; - - private Animal simba; - private Animal nala; - - @BeforeEach - void setUp() { - simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); - nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); - } - - @Test - @DisplayName("Spy Example 1: Should verify notification was sent when adding animal") - void shouldVerifyNotificationWasSentWhenAddingAnimal() { - // Given - simba.setId(1L); - when(animalService.createAnimal(any(Animal.class))).thenReturn(simba); - - // When - Animal addedAnimal = zooManager.addNewAnimal(simba); - - // Then - assertNotNull(addedAnimal); - verify(notificationService, times(1)).sendEmail( - eq("staff@zoo.com"), - eq("New Animal Added"), - contains("Simba") - ); - } - - @Test - @DisplayName("Spy Example 2: Should verify SMS was sent when removing animal") - void shouldVerifySMSWasSentWhenRemovingAnimal() { - // Given - simba.setId(1L); - when(animalService.getAnimalById(1L)).thenReturn(Optional.of(simba)); - when(animalService.deleteAnimal(1L)).thenReturn(true); - - // When - boolean removed = zooManager.removeAnimal(1L); - - // Then - assertTrue(removed); - verify(notificationService, times(1)).sendSMS( - eq("+1234567890"), - contains("Simba") - ); - } - - @Test - @DisplayName("Spy Example 3: Should verify email was sent for unhealthy animal") - void shouldVerifyEmailWasSentForUnhealthyAnimal() { - // Given - simba.setId(1L); - simba.setHealthStatus("Sick"); - when(animalService.isAnimalHealthy(1L)).thenReturn(false); - - // When - zooManager.checkAnimalHealth(1L); - - // Then - verify(notificationService, times(1)).sendEmail( - eq("vet@zoo.com"), - eq("Animal Health Alert"), - contains("1") - ); - } - - @Test - @DisplayName("Spy Example 4: Should not send notification for healthy animal") - void shouldNotSendNotificationForHealthyAnimal() { - // Given - simba.setId(1L); - simba.setHealthStatus("Healthy"); - when(animalService.isAnimalHealthy(1L)).thenReturn(true); - - // When - zooManager.checkAnimalHealth(1L); - - // Then - verify(notificationService, never()).sendEmail(any(), any(), any()); - } - - @Test - @DisplayName("Spy Example 6: Should verify email service availability check") - void shouldVerifyEmailServiceAvailabilityCheck() { - // Given - when(notificationService.isEmailServiceAvailable()).thenReturn(true); - - // When - boolean isAvailable = notificationService.isEmailServiceAvailable(); - - // Then - assertTrue(isAvailable); - verify(notificationService, times(1)).isEmailServiceAvailable(); - } - - @Test - @DisplayName("Spy Example 7: Should verify multiple notifications for multiple animals") - void shouldVerifyMultipleNotificationsForMultipleAnimals() { - // Given - simba.setId(1L); - nala.setId(2L); - when(animalService.createAnimal(any(Animal.class))).thenReturn(simba).thenReturn(nala); - - // When - zooManager.addNewAnimal(simba); - zooManager.addNewAnimal(nala); - - // Then - verify(notificationService, times(2)).sendEmail( - eq("staff@zoo.com"), - eq("New Animal Added"), - any() - ); - } - - @Test - @DisplayName("Spy Example 8: Should verify notification parameters") - void shouldVerifyNotificationParameters() { - // Given - simba.setId(1L); - when(animalService.createAnimal(any(Animal.class))).thenReturn(simba); - - // When - zooManager.addNewAnimal(simba); - - // Then - verify(notificationService).sendEmail( - "staff@zoo.com", - "New Animal Added", - "New animal Simba has been added to the zoo." - ); - } - - @Test - @DisplayName("Spy Example 9: Should verify no notifications for failed operations") - void shouldVerifyNoNotificationsForFailedOperations() { - // Given - when(animalService.getAnimalById(999L)).thenReturn(Optional.empty()); - - // When - boolean removed = zooManager.removeAnimal(999L); - - // Then - assertFalse(removed); - verify(notificationService, never()).sendSMS(any(), any()); - } - - @Test - @DisplayName("Spy Example 10: Should verify notification service interaction") - void shouldVerifyNotificationServiceInteraction() { - // Given - simba.setId(1L); - when(animalService.createAnimal(any(Animal.class))).thenReturn(simba); - - // When - Animal result = zooManager.addNewAnimal(simba); - - // Then - assertNotNull(result); - verify(notificationService, atLeastOnce()).sendEmail(any(), any(), any()); - } -} \ No newline at end of file diff --git a/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java b/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java deleted file mode 100644 index bc2275d..0000000 --- a/lab0.5/src/test/java/com/tddacademy/zoo/service/StubExamplesTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class StubExamplesTest { - - @Mock - private AnimalRepository animalRepository; - - @InjectMocks - private AnimalService animalService; - - private Animal simba; - private Animal nala; - private Animal timon; - - @BeforeEach - void setUp() { - simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); - nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); - timon = new Animal("Timon", "Meerkat", 2.5, LocalDate.of(2021, 3, 10), "Healthy"); - } - - @Test - @DisplayName("Stub Example 1: Should find animals by species using stub") - void shouldFindAnimalsBySpeciesUsingStub() { - // Given - Create a stub that returns predefined data - List lions = Arrays.asList(simba, nala); - when(animalRepository.findBySpecies("Lion")).thenReturn(lions); - - // When - List foundLions = animalService.getAnimalsBySpecies("Lion"); - - // Then - assertEquals(2, foundLions.size()); - assertTrue(foundLions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); - } - - @Test - @DisplayName("Stub Example 2: Should return empty list for non-existent species") - void shouldReturnEmptyListForNonExistentSpecies() { - // Given - Stub returns empty list - when(animalRepository.findBySpecies("Dragon")).thenReturn(Arrays.asList()); - - // When - List foundAnimals = animalService.getAnimalsBySpecies("Dragon"); - - // Then - assertTrue(foundAnimals.isEmpty()); - } - - @Test - @DisplayName("Stub Example 3: Should get animal count using stub") - void shouldGetAnimalCountUsingStub() { - // Given - Stub returns a fixed count - when(animalRepository.count()).thenReturn(10); - - // When - int count = animalService.getAnimalCount(); - - // Then - assertEquals(10, count); - } - - @Test - @DisplayName("Stub Example 4: Should calculate average weight with stub data") - void shouldCalculateAverageWeightWithStubData() { - // Given - Stub returns predefined animals - List animals = Arrays.asList(simba, nala, timon); - when(animalRepository.findAll()).thenReturn(animals); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - // (180.5 + 160.0 + 2.5) / 3 = 114.33 - assertEquals(114.33, averageWeight, 0.01); - } - - @Test - @DisplayName("Stub Example 5: Should handle healthy animal check with stub") - void shouldHandleHealthyAnimalCheckWithStub() { - // Given - Stub returns a healthy animal - simba.setId(1L); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - boolean isHealthy = animalService.isAnimalHealthy(1L); - - // Then - assertTrue(isHealthy); - } - - @Test - @DisplayName("Stub Example 6: Should handle sick animal check with stub") - void shouldHandleSickAnimalCheckWithStub() { - // Given - Stub returns a sick animal - simba.setId(1L); - simba.setHealthStatus("Sick"); - when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - - // When - boolean isHealthy = animalService.isAnimalHealthy(1L); - - // Then - assertFalse(isHealthy); - } - - @Test - @DisplayName("Stub Example 7: Should create animal with stub response") - void shouldCreateAnimalWithStubResponse() { - // Given - Stub returns the saved animal with ID - simba.setId(1L); - when(animalRepository.save(any(Animal.class))).thenReturn(simba); - - // When - Animal createdAnimal = animalService.createAnimal(simba); - - // Then - assertNotNull(createdAnimal.getId()); - assertEquals("Simba", createdAnimal.getName()); - } - - @Test - @DisplayName("Stub Example 8: Should delete animal successfully with stub") - void shouldDeleteAnimalSuccessfullyWithStub() { - // Given - Stub confirms animal exists - when(animalRepository.existsById(1L)).thenReturn(true); - doNothing().when(animalRepository).deleteById(1L); - - // When - boolean deleted = animalService.deleteAnimal(1L); - - // Then - assertTrue(deleted); - } - - @Test - @DisplayName("Stub Example 9: Should fail to delete non-existent animal with stub") - void shouldFailToDeleteNonExistentAnimalWithStub() { - // Given - Stub confirms animal doesn't exist - when(animalRepository.existsById(999L)).thenReturn(false); - - // When - boolean deleted = animalService.deleteAnimal(999L); - - // Then - assertFalse(deleted); - } - - @Test - @DisplayName("Stub Example 10: Should get all animals with stub data") - void shouldGetAllAnimalsWithStubData() { - // Given - Stub returns predefined list - List allAnimals = Arrays.asList(simba, nala, timon); - when(animalRepository.findAll()).thenReturn(allAnimals); - - // When - List result = animalService.getAllAnimals(); - - // Then - assertEquals(3, result.size()); - assertEquals("Simba", result.get(0).getName()); - assertEquals("Nala", result.get(1).getName()); - assertEquals("Timon", result.get(2).getName()); - } - - @Test - @DisplayName("Stub Example 11: Should handle empty repository with stub") - void shouldHandleEmptyRepositoryWithStub() { - // Given - Stub returns empty list - when(animalRepository.findAll()).thenReturn(Arrays.asList()); - - // When - List result = animalService.getAllAnimals(); - - // Then - assertTrue(result.isEmpty()); - } - - @Test - @DisplayName("Stub Example 12: Should calculate zero average for empty repository") - void shouldCalculateZeroAverageForEmptyRepository() { - // Given - Stub returns empty list - when(animalRepository.findAll()).thenReturn(Arrays.asList()); - - // When - double averageWeight = animalService.getAverageWeight(); - - // Then - assertEquals(0.0, averageWeight, 0.01); - } -} \ No newline at end of file diff --git a/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java b/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java deleted file mode 100644 index 7ad4425..0000000 --- a/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.tddacademy.zoo.service; - -import com.tddacademy.zoo.model.Animal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class TodoExercisesTest { - - @Mock - private AnimalRepository animalRepository; - - @Spy - private NotificationService notificationService; - - @InjectMocks - private AnimalService animalService; - - @InjectMocks - private ZooManager zooManager; - - private Animal simba; - private Animal nala; - private Animal timon; - - @BeforeEach - void setUp() { - simba = new Animal("Simba", "Lion", 180.5, LocalDate.of(2020, 5, 15), "Healthy"); - nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); - timon = new Animal("Timon", "Meerkat", 2.5, LocalDate.of(2021, 3, 10), "Healthy"); - } - - // ========== MOCK EXERCISES ========== - - @Test - @DisplayName("TODO: Mock Exercise 1 - Should find animal by species") - void shouldFindAnimalBySpecies() { - // TODO: Complete this test using mocks - // 1. Mock animalRepository.findBySpecies("Lion") to return a list with simba and nala - // 2. Call animalService.getAnimalsBySpecies("Lion") - // 3. Assert that the result contains 2 animals - // 4. Assert that both animals are lions - - // Your code here: - // when(animalRepository.findBySpecies("Lion")).thenReturn(Arrays.asList(simba, nala)); - // - // List lions = animalService.getAnimalsBySpecies("Lion"); - // - // assertEquals(2, lions.size()); - // assertTrue(lions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); - } - - @Test - @DisplayName("TODO: Mock Exercise 2 - Should handle animal not found") - void shouldHandleAnimalNotFound() { - // TODO: Complete this test using mocks - // 1. Mock animalRepository.findById(999L) to return Optional.empty() - // 2. Call animalService.getAnimalById(999L) - // 3. Assert that the result is empty - - // Your code here: - // when(animalRepository.findById(999L)).thenReturn(Optional.empty()); - // - // Optional result = animalService.getAnimalById(999L); - // - // assertTrue(result.isEmpty()); - } - - @Test - @DisplayName("TODO: Mock Exercise 3 - Should verify repository save was called") - void shouldVerifyRepositorySaveWasCalled() { - // TODO: Complete this test using mocks - // 1. Mock animalRepository.save(any(Animal.class)) to return simba with ID 1 - // 2. Call animalService.createAnimal(simba) - // 3. Verify that animalRepository.save(simba) was called exactly once - - // Your code here: - // simba.setId(1L); - // when(animalRepository.save(any(Animal.class))).thenReturn(simba); - // - // animalService.createAnimal(simba); - // - // verify(animalRepository, times(1)).save(simba); - } - - // ========== STUB EXERCISES ========== - - @Test - @DisplayName("TODO: Stub Exercise 1 - Should calculate average weight with stub data") - void shouldCalculateAverageWeightWithStubData() { - // TODO: Complete this test using stubs - // 1. Create stub data: simba (180.5), nala (160.0), timon (2.5) - // 2. Mock animalRepository.findAll() to return this stub data - // 3. Call animalService.getAverageWeight() - // 4. Assert the average is 114.33 (with 0.01 precision) - - // Your code here: - // List animals = Arrays.asList(simba, nala, timon); - // when(animalRepository.findAll()).thenReturn(animals); - // - // double averageWeight = animalService.getAverageWeight(); - // - // assertEquals(114.33, averageWeight, 0.01); - } - - @Test - @DisplayName("TODO: Stub Exercise 2 - Should handle empty repository with stub") - void shouldHandleEmptyRepositoryWithStub() { - // TODO: Complete this test using stubs - // 1. Mock animalRepository.findAll() to return empty list - // 2. Call animalService.getAverageWeight() - // 3. Assert the result is 0.0 - - // Your code here: - // when(animalRepository.findAll()).thenReturn(Arrays.asList()); - // - // double averageWeight = animalService.getAverageWeight(); - // - // assertEquals(0.0, averageWeight, 0.01); - } - - @Test - @DisplayName("TODO: Stub Exercise 3 - Should get animal count with stub") - void shouldGetAnimalCountWithStub() { - // TODO: Complete this test using stubs - // 1. Mock animalRepository.count() to return 15 - // 2. Call animalService.getAnimalCount() - // 3. Assert the result is 15 - - // Your code here: - // when(animalRepository.count()).thenReturn(15); - // - // int count = animalService.getAnimalCount(); - // - // assertEquals(15, count); - } - - // ========== SPY EXERCISES ========== - - @Test - @DisplayName("TODO: Spy Exercise 1 - Should verify email notification for new animal") - void shouldVerifyEmailNotificationForNewAnimal() { - // TODO: Complete this test using spies - // 1. Mock animalRepository.save(any(Animal.class)) to return simba with ID 1 - // 2. Call zooManager.addNewAnimal(simba) - // 3. Verify that notificationService.sendEmail was called with: - // - to: "staff@zoo.com" - // - subject: "New Animal Added" - // - message containing "Simba" - - // Your code here: - // simba.setId(1L); - // when(animalRepository.save(any(Animal.class))).thenReturn(simba); - // - // zooManager.addNewAnimal(simba); - // - // verify(notificationService, times(1)).sendEmail( - // eq("staff@zoo.com"), - // eq("New Animal Added"), - // contains("Simba") - // ); - } - - @Test - @DisplayName("TODO: Spy Exercise 2 - Should verify SMS notification for animal removal") - void shouldVerifySMSNotificationForAnimalRemoval() { - // TODO: Complete this test using spies - // 1. Mock animalRepository.findById(1L) to return simba with ID 1 - // 2. Mock animalRepository.existsById(1L) to return true - // 3. Mock animalRepository.deleteById(1L) to do nothing - // 4. Call zooManager.removeAnimal(1L) - // 5. Verify that notificationService.sendSMS was called with: - // - phone: "+1234567890" - // - message containing "Simba" - - // Your code here: - // simba.setId(1L); - // when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - // when(animalRepository.existsById(1L)).thenReturn(true); - // doNothing().when(animalRepository).deleteById(1L); - // - // zooManager.removeAnimal(1L); - // - // verify(notificationService, times(1)).sendSMS( - // eq("+1234567890"), - // contains("Simba") - // ); - } - - @Test - @DisplayName("TODO: Spy Exercise 3 - Should verify no notification for healthy animal") - void shouldVerifyNoNotificationForHealthyAnimal() { - // TODO: Complete this test using spies - // 1. Mock animalRepository.findById(1L) to return simba with health status "Healthy" - // 2. Call zooManager.checkAnimalHealth(1L) - // 3. Verify that notificationService.sendEmail was NEVER called - - // Your code here: - // simba.setId(1L); - // simba.setHealthStatus("Healthy"); - // when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - // - // zooManager.checkAnimalHealth(1L); - // - // verify(notificationService, never()).sendEmail(any(), any(), any()); - } - - // ========== ADVANCED EXERCISES ========== - - @Test - @DisplayName("TODO: Advanced Exercise 1 - Should verify multiple repository calls") - void shouldVerifyMultipleRepositoryCalls() { - // TODO: Complete this test using mocks and verification - // 1. Mock animalRepository.findAll() to return list with simba and nala - // 2. Call animalService.getAverageWeight() - // 3. Verify that animalRepository.findAll() was called exactly once - // 4. Assert the average weight is 170.25 - - // Your code here: - // List animals = Arrays.asList(simba, nala); - // when(animalRepository.findAll()).thenReturn(animals); - // - // double averageWeight = animalService.getAverageWeight(); - // - // verify(animalRepository, times(1)).findAll(); - // assertEquals(170.25, averageWeight, 0.01); - } - - @Test - @DisplayName("TODO: Advanced Exercise 2 - Should verify notification parameters exactly") - void shouldVerifyNotificationParametersExactly() { - // TODO: Complete this test using spies and exact parameter matching - // 1. Mock animalRepository.save(any(Animal.class)) to return simba with ID 1 - // 2. Call zooManager.addNewAnimal(simba) - // 3. Verify that notificationService.sendEmail was called with exact parameters: - // - to: "staff@zoo.com" - // - subject: "New Animal Added" - // - message: "New animal Simba has been added to the zoo." - - // Your code here: - // simba.setId(1L); - // when(animalRepository.save(any(Animal.class))).thenReturn(simba); - // - // zooManager.addNewAnimal(simba); - // - // verify(notificationService).sendEmail( - // "staff@zoo.com", - // "New Animal Added", - // "New animal Simba has been added to the zoo." - // ); - } - - @Test - @DisplayName("TODO: Advanced Exercise 3 - Should handle complex scenario with multiple mocks") - void shouldHandleComplexScenarioWithMultipleMocks() { - // TODO: Complete this test using multiple mocks and spies - // 1. Mock animalRepository.findById(1L) to return simba with health status "Sick" - // 2. Call zooManager.checkAnimalHealth(1L) - // 3. Verify that notificationService.sendEmail was called with: - // - to: "vet@zoo.com" - // - subject: "Animal Health Alert" - // - message containing "1" - // 4. Verify that animalRepository.findById(1L) was called exactly once - - // Your code here: - // simba.setId(1L); - // simba.setHealthStatus("Sick"); - // when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); - // - // zooManager.checkAnimalHealth(1L); - // - // verify(notificationService, times(1)).sendEmail( - // eq("vet@zoo.com"), - // eq("Animal Health Alert"), - // contains("1") - // ); - // verify(animalRepository, times(1)).findById(1L); - } -} \ No newline at end of file diff --git a/lab1/gradle/wrapper/gradle-wrapper.properties b/lab1/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a118ea3..0000000 --- a/lab1/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file diff --git a/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java b/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java deleted file mode 100644 index 7ebce57..0000000 --- a/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tddacademy.zoo; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ZooApplication { - - public static void main(String[] args) { - SpringApplication.run(ZooApplication.class, args); - } - -} \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java deleted file mode 100644 index 0cd018a..0000000 --- a/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.tddacademy.zoo; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ZooApplicationTests { - - @Test - void contextLoads() { - // This test verifies that the Spring application context loads successfully - } - -} \ No newline at end of file diff --git a/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties b/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties index ff23a68..a118ea3 100644 --- a/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties +++ b/session1/lab0.5/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/session1/lab0.5/gradlew b/session1/lab0.5/gradlew index 23d15a9..4d629e2 100755 --- a/session1/lab0.5/gradlew +++ b/session1/lab0.5/gradlew @@ -15,8 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# SPDX-License-Identifier: Apache-2.0 -# ############################################################################## # @@ -56,9 +54,7 @@ # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. +# (3) This script is generated from the Gradle template within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # @@ -85,8 +81,7 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +109,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -133,13 +128,10 @@ location of your Java installation." fi else JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi fi # Increase the maximum file descriptors if we can. @@ -147,7 +139,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -155,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,15 +197,15 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. +# * treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. @@ -234,8 +226,7 @@ fi # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. +# that process (while maintaining the separation between arguments). # # This will of course break if any of these variables contains a newline or # an unmatched quote. @@ -248,4 +239,4 @@ eval "set -- $( tr '\n' ' ' )" '"$@"' -exec "$JAVACMD" "$@" +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/session1/lab0.5/gradlew.bat b/session1/lab0.5/gradlew.bat deleted file mode 100644 index 5eed7ee..0000000 --- a/session1/lab0.5/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java index 66f871b..c71d500 100644 --- a/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java +++ b/session1/lab0.5/src/test/java/com/tddacademy/zoo/service/TodoExercisesTest.java @@ -45,7 +45,6 @@ void setUp() { nala = new Animal("Nala", "Lion", 160.0, LocalDate.of(2020, 6, 20), "Healthy"); timon = new Animal("Timon", "Meerkat", 2.5, LocalDate.of(2021, 3, 10), "Healthy"); zooManager = new ZooManager(animalService, notificationService); - } // ========== MOCK EXERCISES ========== @@ -54,17 +53,29 @@ void setUp() { @DisplayName("TODO: Mock Exercise 1 - Should find animal by species") void shouldFindAnimalBySpecies() { // TODO: Complete this test using mocks - when(animalRepository.findBySpecies("Lion")).thenReturn(Arrays.asList(simba, nala)); - List lions = animalService.getAnimalsBySpecies("Lion"); - assertEquals(2, lions.size()); - assertTrue(lions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); + // 1. Mock animalRepository.findBySpecies("Lion") to return a list with simba and nala + // 2. Call animalService.getAnimalsBySpecies("Lion") + // 3. Assert that the result contains 2 animals + // 4. Assert that both animals are lions + + // Your code here: + when(animalRepository.findBySpecies("Lion")).thenReturn(Arrays.asList(simba, nala)); + + List lions = animalService.getAnimalsBySpecies("Lion"); + + assertEquals(2, lions.size()); + assertTrue(lions.stream().allMatch(animal -> "Lion".equals(animal.getSpecies()))); } @Test @DisplayName("TODO: Mock Exercise 2 - Should handle animal not found") void shouldHandleAnimalNotFound() { // TODO: Complete this test using mocks - + // 1. Mock animalRepository.findById(999L) to return Optional.empty() + // 2. Call animalService.getAnimalById(999L) + // 3. Assert that the result is empty + + // Your code here: when(animalRepository.findById(999L)).thenReturn(Optional.empty()); Optional result = animalService.getAnimalById(999L); @@ -76,6 +87,11 @@ void shouldHandleAnimalNotFound() { @DisplayName("TODO: Mock Exercise 3 - Should verify repository save was called") void shouldVerifyRepositorySaveWasCalled() { // TODO: Complete this test using mocks + // 1. Mock animalRepository.save(any(Animal.class)) to return simba with ID 1 + // 2. Call animalService.createAnimal(simba) + // 3. Verify that animalRepository.save(simba) was called exactly once + + // Your code here: simba.setId(1L); when(animalRepository.save(any(Animal.class))).thenReturn(simba); @@ -89,7 +105,13 @@ void shouldVerifyRepositorySaveWasCalled() { @Test @DisplayName("TODO: Stub Exercise 1 - Should calculate average weight with stub data") void shouldCalculateAverageWeightWithStubData() { - + // TODO: Complete this test using stubs + // 1. Create stub data: simba (180.5), nala (160.0), timon (2.5) + // 2. Mock animalRepository.findAll() to return this stub data + // 3. Call animalService.getAverageWeight() + // 4. Assert the average is 114.33 (with 0.01 precision) + + // Your code here: List animals = Arrays.asList(simba, nala, timon); when(animalRepository.findAll()).thenReturn(animals); @@ -102,7 +124,11 @@ void shouldCalculateAverageWeightWithStubData() { @DisplayName("TODO: Stub Exercise 2 - Should handle empty repository with stub") void shouldHandleEmptyRepositoryWithStub() { // TODO: Complete this test using stubs - + // 1. Mock animalRepository.findAll() to return empty list + // 2. Call animalService.getAverageWeight() + // 3. Assert the result is 0.0 + + // Your code here: when(animalRepository.findAll()).thenReturn(Arrays.asList()); double averageWeight = animalService.getAverageWeight(); @@ -114,7 +140,11 @@ void shouldHandleEmptyRepositoryWithStub() { @DisplayName("TODO: Stub Exercise 3 - Should get animal count with stub") void shouldGetAnimalCountWithStub() { // TODO: Complete this test using stubs - + // 1. Mock animalRepository.count() to return 15 + // 2. Call animalService.getAnimalCount() + // 3. Assert the result is 15 + + // Your code here: when(animalRepository.count()).thenReturn(15); int count = animalService.getAnimalCount(); @@ -128,7 +158,14 @@ void shouldGetAnimalCountWithStub() { @DisplayName("TODO: Spy Exercise 1 - Should verify email notification for new animal") void shouldVerifyEmailNotificationForNewAnimal() { // TODO: Complete this test using spies - + // 1. Mock animalRepository.save(any(Animal.class)) to return simba with ID 1 + // 2. Call zooManager.addNewAnimal(simba) + // 3. Verify that notificationService.sendEmail was called with: + // - to: "staff@zoo.com" + // - subject: "New Animal Added" + // - message containing "Simba" + + // Your code here: simba.setId(1L); when(animalRepository.save(any(Animal.class))).thenReturn(simba); @@ -145,7 +182,15 @@ void shouldVerifyEmailNotificationForNewAnimal() { @DisplayName("TODO: Spy Exercise 2 - Should verify SMS notification for animal removal") void shouldVerifySMSNotificationForAnimalRemoval() { // TODO: Complete this test using spies - + // 1. Mock animalRepository.findById(1L) to return simba with ID 1 + // 2. Mock animalRepository.existsById(1L) to return true + // 3. Mock animalRepository.deleteById(1L) to do nothing + // 4. Call zooManager.removeAnimal(1L) + // 5. Verify that notificationService.sendSMS was called with: + // - phone: "+1234567890" + // - message containing "Simba" + + // Your code here: simba.setId(1L); when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); when(animalRepository.existsById(1L)).thenReturn(true); @@ -163,7 +208,11 @@ void shouldVerifySMSNotificationForAnimalRemoval() { @DisplayName("TODO: Spy Exercise 3 - Should verify no notification for healthy animal") void shouldVerifyNoNotificationForHealthyAnimal() { // TODO: Complete this test using spies - + // 1. Mock animalRepository.findById(1L) to return simba with health status "Healthy" + // 2. Call zooManager.checkAnimalHealth(1L) + // 3. Verify that notificationService.sendEmail was NEVER called + + // Your code here: simba.setId(1L); simba.setHealthStatus("Healthy"); when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); @@ -179,7 +228,12 @@ void shouldVerifyNoNotificationForHealthyAnimal() { @DisplayName("TODO: Advanced Exercise 1 - Should verify multiple repository calls") void shouldVerifyMultipleRepositoryCalls() { // TODO: Complete this test using mocks and verification - + // 1. Mock animalRepository.findAll() to return list with simba and nala + // 2. Call animalService.getAverageWeight() + // 3. Verify that animalRepository.findAll() was called exactly once + // 4. Assert the average weight is 170.25 + + // Your code here: List animals = Arrays.asList(simba, nala); when(animalRepository.findAll()).thenReturn(animals); @@ -193,7 +247,14 @@ void shouldVerifyMultipleRepositoryCalls() { @DisplayName("TODO: Advanced Exercise 2 - Should verify notification parameters exactly") void shouldVerifyNotificationParametersExactly() { // TODO: Complete this test using spies and exact parameter matching - + // 1. Mock animalRepository.save(any(Animal.class)) to return simba with ID 1 + // 2. Call zooManager.addNewAnimal(simba) + // 3. Verify that notificationService.sendEmail was called with exact parameters: + // - to: "staff@zoo.com" + // - subject: "New Animal Added" + // - message: "New animal Simba has been added to the zoo." + + // Your code here: simba.setId(1L); when(animalRepository.save(any(Animal.class))).thenReturn(simba); @@ -209,7 +270,16 @@ void shouldVerifyNotificationParametersExactly() { @Test @DisplayName("TODO: Advanced Exercise 3 - Should handle complex scenario with multiple mocks") void shouldHandleComplexScenarioWithMultipleMocks() { - + // TODO: Complete this test using multiple mocks and spies + // 1. Mock animalRepository.findById(1L) to return simba with health status "Sick" + // 2. Call zooManager.checkAnimalHealth(1L) + // 3. Verify that notificationService.sendEmail was called with: + // - to: "vet@zoo.com" + // - subject: "Animal Health Alert" + // - message containing "1" + // 4. Verify that animalRepository.findById(1L) was called exactly once + + // Your code here: simba.setId(1L); simba.setHealthStatus("Sick"); when(animalRepository.findById(1L)).thenReturn(Optional.of(simba)); @@ -223,6 +293,4 @@ void shouldHandleComplexScenarioWithMultipleMocks() { ); verify(animalRepository, times(1)).findById(1L); } - - } \ No newline at end of file diff --git a/lab1/README.md b/session1/lab1/README.md similarity index 100% rename from lab1/README.md rename to session1/lab1/README.md diff --git a/lab1/SUMMARY.md b/session1/lab1/SUMMARY.md similarity index 100% rename from lab1/SUMMARY.md rename to session1/lab1/SUMMARY.md diff --git a/lab1/build.gradle b/session1/lab1/build.gradle similarity index 100% rename from lab1/build.gradle rename to session1/lab1/build.gradle diff --git a/lab0.5/gradle/wrapper/gradle-wrapper.properties b/session1/lab1/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from lab0.5/gradle/wrapper/gradle-wrapper.properties rename to session1/lab1/gradle/wrapper/gradle-wrapper.properties diff --git a/lab1/gradlew b/session1/lab1/gradlew similarity index 100% rename from lab1/gradlew rename to session1/lab1/gradlew diff --git a/lab1/gradlew.bat b/session1/lab1/gradlew.bat similarity index 100% rename from lab1/gradlew.bat rename to session1/lab1/gradlew.bat diff --git a/lab0.5/src/main/java/com/tddacademy/zoo/ZooApplication.java b/session1/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java similarity index 100% rename from lab0.5/src/main/java/com/tddacademy/zoo/ZooApplication.java rename to session1/lab1/src/main/java/com/tddacademy/zoo/ZooApplication.java diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Animal.java b/session1/lab1/src/main/java/com/tddacademy/zoo/model/Animal.java similarity index 100% rename from lab1/src/main/java/com/tddacademy/zoo/model/Animal.java rename to session1/lab1/src/main/java/com/tddacademy/zoo/model/Animal.java diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java b/session1/lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java similarity index 100% rename from lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java rename to session1/lab1/src/main/java/com/tddacademy/zoo/model/Enclosure.java diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Person.java b/session1/lab1/src/main/java/com/tddacademy/zoo/model/Person.java similarity index 100% rename from lab1/src/main/java/com/tddacademy/zoo/model/Person.java rename to session1/lab1/src/main/java/com/tddacademy/zoo/model/Person.java diff --git a/lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java b/session1/lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java similarity index 100% rename from lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java rename to session1/lab1/src/main/java/com/tddacademy/zoo/model/Zoo.java diff --git a/lab1/src/main/resources/application.properties b/session1/lab1/src/main/resources/application.properties similarity index 100% rename from lab1/src/main/resources/application.properties rename to session1/lab1/src/main/resources/application.properties diff --git a/lab0.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/session1/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java similarity index 100% rename from lab0.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java rename to session1/lab1/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java b/session1/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java similarity index 68% rename from lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java rename to session1/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java index a288762..47a2635 100644 --- a/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java +++ b/session1/lab1/src/test/java/com/tddacademy/zoo/model/AnimalTest.java @@ -63,19 +63,19 @@ void shouldThrowExceptionWhenNameIsNull() { // 3. Verify the exception message is "Animal name cannot be null or empty" // Your code here: - // Long id = 1L; - // String name = null; - // String species = "Lion"; - // String breed = "African Lion"; - // LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); - // Double weight = 180.5; - // String healthStatus = "Healthy"; - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) - // ); - // assertEquals("Animal name cannot be null or empty", exception.getMessage()); + Long id = 1L; + String name = null; + String species = "Lion"; + String breed = "African Lion"; + LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + Double weight = 145.8; + String healthStatus = "Healthy"; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + ); + } @Test @@ -87,19 +87,19 @@ void shouldThrowExceptionWhenSpeciesIsNull() { // 3. Verify the exception message is "Animal species cannot be null or empty" // Your code here: - // Long id = 1L; - // String name = "Simba"; - // String species = null; - // String breed = "African Lion"; - // LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); - // Double weight = 180.5; - // String healthStatus = "Healthy"; - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) - // ); - // assertEquals("Animal species cannot be null or empty", exception.getMessage()); + Long id = 1L; + String name = "Simba"; + String species = null; + String breed = "African Lion"; + LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + Double weight = 180.5; + String healthStatus = "Healthy"; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + ); + assertEquals("Animal species cannot be null or empty", exception.getMessage()); } @Test @@ -131,18 +131,18 @@ void shouldThrowExceptionWhenWeightIsZero() { // 3. Verify the exception message is "Animal weight must be positive" // Your code here: - // Long id = 1L; - // String name = "Simba"; - // String species = "Lion"; - // String breed = "African Lion"; - // LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); - // Double weight = 0.0; - // String healthStatus = "Healthy"; - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) - // ); - // assertEquals("Animal weight must be positive", exception.getMessage()); + Long id = 1L; + String name = "Simba"; + String species = "Lion"; + String breed = "African Lion"; + LocalDate dateOfBirth = LocalDate.of(2020, 5, 15); + Double weight = 0.0; + String healthStatus = "Healthy"; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Animal(id, name, species, breed, dateOfBirth, weight, healthStatus) + ); + assertEquals("Animal weight must be positive", exception.getMessage()); } } \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java b/session1/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java similarity index 89% rename from lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java rename to session1/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java index cbf5dd2..729609b 100644 --- a/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java +++ b/session1/lab1/src/test/java/com/tddacademy/zoo/model/EnclosureTest.java @@ -1,5 +1,6 @@ package com.tddacademy.zoo.model; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import static org.junit.jupiter.api.Assertions.*; @@ -103,18 +104,18 @@ void shouldThrowExceptionWhenAreaIsNegative() { // 3. Verify the exception message is "Enclosure area must be positive" // Your code here: - // Long id = 1L; - // String name = "Lion Habitat"; - // String type = "Savanna"; - // Double area = -100.0; - // String climate = "Tropical"; - // List animals = new ArrayList<>(); - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Enclosure(id, name, type, area, climate, animals) - // ); - // assertEquals("Enclosure area must be positive", exception.getMessage()); + Long id = 1L; + String name = "Lion Habitat"; + String type = "Savanna"; + Double area = -100.0; + String climate = "Tropical"; + List animals = new ArrayList<>(); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Enclosure(id, name, type, area, climate, animals) + ); + assertEquals("Enclosure area must be positive", exception.getMessage()); } @Test diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java b/session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java similarity index 65% rename from lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java rename to session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java index ccd14f7..d2e4673 100644 --- a/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java +++ b/session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java @@ -18,24 +18,24 @@ void shouldCreateValidPerson() { // 4. Assert that all fields match the expected values // Your code here: - // Long id = 1L; - // String firstName = "John"; - // String lastName = "Doe"; - // String role = "Zookeeper"; - // String email = "john.doe@zoo.com"; - // LocalDate hireDate = LocalDate.of(2023, 1, 15); - // Double salary = 45000.0; - // - // Person person = new Person(id, firstName, lastName, role, email, hireDate, salary); - // - // assertNotNull(person); - // assertEquals(id, person.id()); - // assertEquals(firstName, person.firstName()); - // assertEquals(lastName, person.lastName()); - // assertEquals(role, person.role()); - // assertEquals(email, person.email()); - // assertEquals(hireDate, person.hireDate()); - // assertEquals(salary, person.salary()); + Long id = 1L; + String firstName = "John"; + String lastName = "Doe"; + String role = "Zookeeper"; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = 45000.0; + + Person person = new Person(id, firstName, lastName, role, email, hireDate, salary); + + assertNotNull(person); + assertEquals(id, person.id()); + assertEquals(firstName, person.firstName()); + assertEquals(lastName, person.lastName()); + assertEquals(role, person.role()); + assertEquals(email, person.email()); + assertEquals(hireDate, person.hireDate()); + assertEquals(salary, person.salary()); } @Test @@ -67,19 +67,19 @@ void shouldThrowExceptionWhenFirstNameIsNull() { // 3. Verify the exception message is "Person first name cannot be null or empty" // Your code here: - // Long id = 1L; - // String firstName = null; - // String lastName = "Doe"; - // String role = "Zookeeper"; - // String email = "john.doe@zoo.com"; - // LocalDate hireDate = LocalDate.of(2023, 1, 15); - // Double salary = 45000.0; - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Person(id, firstName, lastName, role, email, hireDate, salary) - // ); - // assertEquals("Person first name cannot be null or empty", exception.getMessage()); + Long id = 1L; + String firstName = null; + String lastName = "Doe"; + String role = "Zookeeper"; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = 45000.0; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + ); + assertEquals("Person first name cannot be null or empty", exception.getMessage()); } @Test @@ -111,19 +111,19 @@ void shouldThrowExceptionWhenRoleIsNull() { // 3. Verify the exception message is "Person role cannot be null or empty" // Your code here: - // Long id = 1L; - // String firstName = "John"; - // String lastName = "Doe"; - // String role = null; - // String email = "john.doe@zoo.com"; - // LocalDate hireDate = LocalDate.of(2023, 1, 15); - // Double salary = 45000.0; - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Person(id, firstName, lastName, role, email, hireDate, salary) - // ); - // assertEquals("Person role cannot be null or empty", exception.getMessage()); + Long id = 1L; + String firstName = "John"; + String lastName = "Doe"; + String role = null; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = 45000.0; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + ); + assertEquals("Person role cannot be null or empty", exception.getMessage()); } @Test @@ -175,18 +175,18 @@ void shouldThrowExceptionWhenSalaryIsZero() { // 3. Verify the exception message is "Person salary must be positive" // Your code here: - // Long id = 1L; - // String firstName = "John"; - // String lastName = "Doe"; - // String role = "Zookeeper"; - // String email = "john.doe@zoo.com"; - // LocalDate hireDate = LocalDate.of(2023, 1, 15); - // Double salary = 0.0; - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> new Person(id, firstName, lastName, role, email, hireDate, salary) - // ); - // assertEquals("Person salary must be positive", exception.getMessage()); + Long id = 1L; + String firstName = "John"; + String lastName = "Doe"; + String role = "Zookeeper"; + String email = "john.doe@zoo.com"; + LocalDate hireDate = LocalDate.of(2023, 1, 15); + Double salary = 0.0; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Person(id, firstName, lastName, role, email, hireDate, salary) + ); + assertEquals("Person salary must be positive", exception.getMessage()); } } \ No newline at end of file diff --git a/lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java b/session1/lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java similarity index 100% rename from lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java rename to session1/lab1/src/test/java/com/tddacademy/zoo/model/ZooTest.java From d84bf50df03486cb66bcdd92a1ee322d189f63d7 Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Mon, 21 Jul 2025 19:43:59 +0800 Subject: [PATCH 5/8] vincent and joshua final submittion lab1 --- .../lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java b/session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java index d2e4673..9d4cce9 100644 --- a/session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java +++ b/session1/lab1/src/test/java/com/tddacademy/zoo/model/PersonTest.java @@ -12,6 +12,7 @@ class PersonTest { @DisplayName("Should create a valid Person with all required fields") void shouldCreateValidPerson() { // TODO: Complete this test + // 1. Create test data with all required fields // 2. Create a Person object with the test data // 3. Assert that the person is not null From 0c84b5291a185088a5291ad8085a61927e7e5a3a Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Wed, 23 Jul 2025 18:33:19 +0800 Subject: [PATCH 6/8] vincent and joshua lab 1.5 final submission --- {lab1.5 => session1/lab1.5}/README.md | 0 {lab1.5 => session1/lab1.5}/SUMMARY.md | 0 {lab1.5 => session1/lab1.5}/build.gradle | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 {lab1.5 => session1/lab1.5}/gradlew | 0 .../java/com/tddacademy/zoo/ZooApplication.java | 0 .../tddacademy/zoo/controller/ZooController.java | 0 .../java/com/tddacademy/zoo/model/Animal.java | 0 .../java/com/tddacademy/zoo/model/Enclosure.java | 0 .../java/com/tddacademy/zoo/model/Person.java | 0 .../main/java/com/tddacademy/zoo/model/Zoo.java | 0 .../com/tddacademy/zoo/service/ZooService.java | 0 .../src/main/resources/application.properties | 0 .../com/tddacademy/zoo/ZooApplicationTests.java | 0 .../zoo/controller/ZooControllerTest.java | 16 ++++++++++------ 15 files changed, 10 insertions(+), 6 deletions(-) rename {lab1.5 => session1/lab1.5}/README.md (100%) rename {lab1.5 => session1/lab1.5}/SUMMARY.md (100%) rename {lab1.5 => session1/lab1.5}/build.gradle (100%) rename {lab1.5 => session1/lab1.5}/gradle/wrapper/gradle-wrapper.properties (100%) rename {lab1.5 => session1/lab1.5}/gradlew (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/ZooApplication.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/controller/ZooController.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/model/Animal.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/model/Enclosure.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/model/Person.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/model/Zoo.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/java/com/tddacademy/zoo/service/ZooService.java (100%) rename {lab1.5 => session1/lab1.5}/src/main/resources/application.properties (100%) rename {lab1.5 => session1/lab1.5}/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java (100%) rename {lab1.5 => session1/lab1.5}/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java (81%) diff --git a/lab1.5/README.md b/session1/lab1.5/README.md similarity index 100% rename from lab1.5/README.md rename to session1/lab1.5/README.md diff --git a/lab1.5/SUMMARY.md b/session1/lab1.5/SUMMARY.md similarity index 100% rename from lab1.5/SUMMARY.md rename to session1/lab1.5/SUMMARY.md diff --git a/lab1.5/build.gradle b/session1/lab1.5/build.gradle similarity index 100% rename from lab1.5/build.gradle rename to session1/lab1.5/build.gradle diff --git a/lab1.5/gradle/wrapper/gradle-wrapper.properties b/session1/lab1.5/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from lab1.5/gradle/wrapper/gradle-wrapper.properties rename to session1/lab1.5/gradle/wrapper/gradle-wrapper.properties diff --git a/lab1.5/gradlew b/session1/lab1.5/gradlew similarity index 100% rename from lab1.5/gradlew rename to session1/lab1.5/gradlew diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/ZooApplication.java diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/controller/ZooController.java diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Animal.java diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Enclosure.java diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Person.java diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/model/Zoo.java diff --git a/lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java b/session1/lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java similarity index 100% rename from lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java rename to session1/lab1.5/src/main/java/com/tddacademy/zoo/service/ZooService.java diff --git a/lab1.5/src/main/resources/application.properties b/session1/lab1.5/src/main/resources/application.properties similarity index 100% rename from lab1.5/src/main/resources/application.properties rename to session1/lab1.5/src/main/resources/application.properties diff --git a/lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java b/session1/lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java similarity index 100% rename from lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java rename to session1/lab1.5/src/test/java/com/tddacademy/zoo/ZooApplicationTests.java diff --git a/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java b/session1/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java similarity index 81% rename from lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java rename to session1/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java index 3647305..454fa87 100644 --- a/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java +++ b/session1/lab1.5/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java @@ -48,10 +48,14 @@ void shouldReturnZooWhenItExists() throws Exception { // - jsonPath("$.description").value("A beautiful zoo in the heart of Manila") // Your code here: - // mockMvc.perform(get("/api/zoos/1")) - // .andExpect(...) - // .andExpect(...) - // .andExpect(...); + mockMvc.perform(get("/api/zoos/1")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")) + .andExpect(jsonPath("$.location").value("Manila, Philippines")) + .andExpect(jsonPath("$.description").value("A beautiful zoo in the heart of Manila")); + } @Test @@ -62,7 +66,7 @@ void shouldReturn404WhenZooDoesNotExist() throws Exception { // 2. Add expectation for status().isNotFound() // Your code here: - // mockMvc.perform(get("/api/zoos/999")) - // .andExpect(...); + mockMvc.perform(get("/api/zoos/999")) + .andExpect(status().isNotFound()); } } \ No newline at end of file From f12a40a4c359fc64a4fd56d2ea72d9ba95830640 Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Wed, 23 Jul 2025 21:54:41 +0800 Subject: [PATCH 7/8] Vincent + Joshua: Final submission, Lab 2 --- .../zoo/controller/ZooControllerTest.java | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java b/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java index f54c64b..1037374 100644 --- a/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java +++ b/lab2/src/test/java/com/tddacademy/zoo/controller/ZooControllerTest.java @@ -39,10 +39,10 @@ class ZooControllerTest { @BeforeEach void setUp() { - testZoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", - "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); - createdZoo = new Zoo(1L, "Manila Zoo", "Manila, Philippines", - "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + testZoo = new Zoo(null, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); + createdZoo = new Zoo(1L, "Manila Zoo", "Manila, Philippines", + "A beautiful zoo in the heart of Manila", new ArrayList<>(), new ArrayList<>()); } @Test @@ -88,13 +88,17 @@ void shouldReturnZooWhenItExists() throws Exception { // - jsonPath("$.name").value("Manila Zoo") // - jsonPath("$.location").value("Manila, Philippines") // - jsonPath("$.description").value("A beautiful zoo in the heart of Manila") - + // Your code here: - // when(zooService.getZooById(1L)).thenReturn(createdZoo); - // mockMvc.perform(get("/api/zoos/1")) - // .andExpect(...) - // .andExpect(...) - // .andExpect(...); + when(zooService.getZooById(1L)).thenReturn(createdZoo); + + mockMvc.perform(get("/api/zoos/1")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Manila Zoo")) + .andExpect(jsonPath("$.location").value("Manila, Philippines")) + .andExpect(jsonPath("$.description").value("A beautiful zoo in the heart of Manila")); } @Test @@ -140,16 +144,20 @@ void shouldUpdateZooSuccessfully() throws Exception { // - jsonPath("$.name").value("Updated Zoo Name") // - jsonPath("$.location").value("Updated Location") // - jsonPath("$.description").value("Updated description") - + // Your code here: - // Zoo updatedZoo = new Zoo(1L, "Updated Zoo Name", "Updated Location", "Updated description", new ArrayList<>(), new ArrayList<>()); - // when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); - // mockMvc.perform(put("/api/zoos/1") - // .contentType(MediaType.APPLICATION_JSON) - // .content(objectMapper.writeValueAsString(testZoo))) - // .andExpect(...) - // .andExpect(...) - // .andExpect(...); + Zoo updatedZoo = new Zoo(1L, "Updated Zoo Name", "Updated Location", "Updated description", new ArrayList<>(), new ArrayList<>()); + when(zooService.updateZoo(eq(1L), any(Zoo.class))).thenReturn(updatedZoo); + + mockMvc.perform(put("/api/zoos/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Updated Zoo Name")) + .andExpect(jsonPath("$.location").value("Updated Location")) + .andExpect(jsonPath("$.description").value("Updated description")); } @Test @@ -159,13 +167,13 @@ void shouldReturn404WhenUpdatingNonExistentZoo() throws Exception { // 1. Set up the mock to throw an exception: when(zooService.updateZoo(eq(999L), any(Zoo.class))).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); // 2. Use mockMvc.perform(put("/api/zoos/999").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(testZoo))) // 3. Add expectation for status().isNotFound() - + // Your code here: - // when(zooService.updateZoo(eq(999L), any(Zoo.class))).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); - // mockMvc.perform(put("/api/zoos/999") - // .contentType(MediaType.APPLICATION_JSON) - // .content(objectMapper.writeValueAsString(testZoo))) - // .andExpect(...); + when(zooService.updateZoo(eq(999L), any(Zoo.class))).thenThrow(new IllegalArgumentException("Zoo not found with id: 999")); + mockMvc.perform(put("/api/zoos/999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testZoo))) + .andExpect(status().isNotFound()); } @Test @@ -175,11 +183,11 @@ void shouldDeleteZooSuccessfully() throws Exception { // 1. Set up the mock: doNothing().when(zooService).deleteZoo(1L); // 2. Use mockMvc.perform(delete("/api/zoos/1")) to make a DELETE request // 3. Add expectation for status().isNoContent() - + // Your code here: - // doNothing().when(zooService).deleteZoo(1L); - // mockMvc.perform(delete("/api/zoos/1")) - // .andExpect(...); + doNothing().when(zooService).deleteZoo(1L); + mockMvc.perform(delete("/api/zoos/1")) + .andExpect(status().isNoContent()); } @Test @@ -189,11 +197,11 @@ void shouldReturn404WhenDeletingNonExistentZoo() throws Exception { // 1. Set up the mock to throw an exception: doThrow(new IllegalArgumentException("Zoo not found with id: 999")).when(zooService).deleteZoo(999L); // 2. Use mockMvc.perform(delete("/api/zoos/999")) to make a DELETE request // 3. Add expectation for status().isNotFound() - + // Your code here: - // doThrow(new IllegalArgumentException("Zoo not found with id: 999")).when(zooService).deleteZoo(999L); - // mockMvc.perform(delete("/api/zoos/999")) - // .andExpect(...); + doThrow(new IllegalArgumentException("Zoo not found with id: 999")).when(zooService).deleteZoo(999L); + mockMvc.perform(delete("/api/zoos/999")) + .andExpect(status().isNotFound()); } @Test @@ -212,12 +220,12 @@ void shouldHandleMalformedJsonInPutRequest() throws Exception { // TODO: Complete this test // 1. Use mockMvc.perform(put("/api/zoos/1").contentType(MediaType.APPLICATION_JSON).content("{ invalid json }")) // 2. Add expectation for status().isBadRequest() - + // Your code here: - // mockMvc.perform(put("/api/zoos/1") - // .contentType(MediaType.APPLICATION_JSON) - // .content("{ invalid json }")) - // .andExpect(...); + mockMvc.perform(put("/api/zoos/1") + .contentType(MediaType.APPLICATION_JSON) + .content("{ invalid json }")) + .andExpect(status().isBadRequest()); } @Test @@ -250,4 +258,4 @@ void shouldReturnProperContentTypeForAllResponses() throws Exception { mockMvc.perform(delete("/api/zoos/1")) .andExpect(status().isNoContent()); } -} \ No newline at end of file +} \ No newline at end of file From e80f7e9871c5fd8e71068506c3dcd8bd3f554dee Mon Sep 17 00:00:00 2001 From: vincent+joshua Date: Wed, 23 Jul 2025 22:33:25 +0800 Subject: [PATCH 8/8] Vincent + Joshua: Final submission, Lab 2 --- .../zoo/service/ZooServiceTest.java | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java b/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java index 590654f..80ea9bf 100644 --- a/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java +++ b/lab3/src/test/java/com/tddacademy/zoo/service/ZooServiceTest.java @@ -111,17 +111,17 @@ void shouldUpdateZooWhenExists() { // 7. Verify that zooRepository.save was called once // Your code here: - // Long zooId = 1L; - // manilaZoo.setId(zooId); - // Zoo updatedZoo = new Zoo("Updated Manila Zoo", "Updated Location", "Updated description"); - // - // when(zooRepository.findById(zooId)).thenReturn(Optional.of(manilaZoo)); - // when(zooRepository.save(any(Zoo.class))).thenReturn(updatedZoo); - // - // Zoo result = zooService.updateZoo(zooId, updatedZoo); - // - // assertEquals("Updated Manila Zoo", result.getName()); - // verify(zooRepository, times(1)).save(any(Zoo.class)); + Long zooId = 1L; + manilaZoo.setId(zooId); + Zoo updatedZoo = new Zoo("Updated Manila Zoo", "Updated Location", "Updated description"); + + when(zooRepository.findById(zooId)).thenReturn(Optional.of(manilaZoo)); + when(zooRepository.save(any(Zoo.class))).thenReturn(updatedZoo); + + Zoo result = zooService.updateZoo(zooId, updatedZoo); + + assertEquals("Updated Manila Zoo", result.getName()); + verify(zooRepository, times(1)).save(any(Zoo.class)); } @Test @@ -134,16 +134,16 @@ void shouldThrowExceptionWhenUpdatingNonExistentZoo() { // 4. Verify the exception message contains "Zoo not found with id: 999" // Your code here: - // Long zooId = 999L; - // Zoo updatedZoo = new Zoo("Updated Zoo", "Updated Location", "Updated description"); - // - // when(zooRepository.findById(zooId)).thenReturn(Optional.empty()); - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> zooService.updateZoo(zooId, updatedZoo) - // ); - // assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); + Long zooId = 999L; + Zoo updatedZoo = new Zoo("Updated Zoo", "Updated Location", "Updated description"); + + when(zooRepository.findById(zooId)).thenReturn(Optional.empty()); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.updateZoo(zooId, updatedZoo) + ); + assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); } @Test @@ -155,12 +155,12 @@ void shouldDeleteZooWhenExists() { // 3. Verify that zooRepository.deleteById(1L) was called once // Your code here: - // Long zooId = 1L; - // when(zooRepository.existsById(zooId)).thenReturn(true); - // - // zooService.deleteZoo(zooId); - // - // verify(zooRepository, times(1)).deleteById(zooId); + Long zooId = 1L; + when(zooRepository.existsById(zooId)).thenReturn(true); + + zooService.deleteZoo(zooId); + + verify(zooRepository, times(1)).deleteById(zooId); } @Test @@ -172,14 +172,14 @@ void shouldThrowExceptionWhenDeletingNonExistentZoo() { // 3. Verify the exception message contains "Zoo not found with id: 999" // Your code here: - // Long zooId = 999L; - // when(zooRepository.existsById(zooId)).thenReturn(false); - // - // IllegalArgumentException exception = assertThrows( - // IllegalArgumentException.class, - // () -> zooService.deleteZoo(zooId) - // ); - // assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); + Long zooId = 999L; + when(zooRepository.existsById(zooId)).thenReturn(false); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> zooService.deleteZoo(zooId) + ); + assertTrue(exception.getMessage().contains("Zoo not found with id: 999")); } @Test