diff --git a/.gitignore b/.gitignore index 5621a8b..b78592b 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ hs_err_*.log *.synctex.gz *.toc *.xdv + +# jqwik +.jqwik-database diff --git a/pom.xml b/pom.xml index 3133ecf..f3e5792 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,13 @@ SPDX-FileCopyrightText: 2023 swt23w23 runtime + + net.jqwik + jqwik + 1.8.2 + test + + diff --git a/src/test/java/catering/inventory/InventoryMutateFormUnitTests.java b/src/test/java/catering/inventory/InventoryMutateFormUnitTests.java new file mode 100644 index 0000000..b45bd93 --- /dev/null +++ b/src/test/java/catering/inventory/InventoryMutateFormUnitTests.java @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2024 swt23w23 +package catering.inventory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.salespointframework.core.Currencies.EURO; + +import java.util.Optional; +import java.util.Set; + +import javax.money.MonetaryAmount; +import javax.money.NumberValue; + +import org.javamoney.moneta.Money; +import org.junit.jupiter.api.Test; +import org.salespointframework.catalog.Product; +import org.salespointframework.inventory.UniqueInventoryItem; +import org.salespointframework.quantity.Metric; +import org.salespointframework.quantity.Quantity; + +import catering.catalog.Consumable; +import catering.catalog.Rentable; +import catering.order.OrderType; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Assume; +import net.jqwik.api.Combinators; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import net.jqwik.api.constraints.NotBlank; + +public class InventoryMutateFormUnitTests { + /* + * Providers for jqwik + */ + + @Provide + Arbitrary euroMonetaryAmount() { + return Arbitraries.doubles() + .filter(amount -> amount >= 0) + .map(amount -> Money.of(amount, EURO)); + } + + @Provide + Arbitrary> optionalEuroMonetaryAmount() { + return euroMonetaryAmount().optional(); + } + + @Provide + Arbitrary positiveOrZero() { + return Arbitraries.longs().filter(x -> x >= 0); + } + + @Provide + Arbitrary consumable() { + return Combinators + .combine(Arbitraries.strings().filter(s -> !s.isBlank()), euroMonetaryAmount(), euroMonetaryAmount(), + optionalEuroMonetaryAmount(), Arbitraries.subsetOf(OrderType.values()), + Arbitraries.of(Metric.class)) + .as((name, retailPrice, wholesalePrice, promotionPrice, orderTypes, metric) -> new Consumable(name, + retailPrice, wholesalePrice, promotionPrice, orderTypes, metric)); + } + + @Provide + Arbitrary rentable() { + return Combinators + .combine(Arbitraries.strings().filter(s -> !s.isBlank()), euroMonetaryAmount(), euroMonetaryAmount(), + Arbitraries.subsetOf(OrderType.values()), + Arbitraries.of(Metric.class)) + .as((name, retailPrice, wholesalePrice, orderTypes, metric) -> new Rentable(name, retailPrice, + wholesalePrice, orderTypes, metric)); + } + + Arbitrary item(Arbitrary product) { + return Combinators.combine(product, positiveOrZero()) + .as((prod, qty) -> new UniqueInventoryItem(prod, prod.createQuantity(qty))); + } + + @Provide + Arbitrary consumableItem() { + return item(consumable()); + } + + @Provide + Arbitrary rentableItem() { + return item(rentable()); + } + + /* + * Property-based tests with jqwik + */ + + @Property + void property_formToConsumableToFormIsIdentity(@ForAll @NotBlank String name, + @ForAll("positiveOrZero") long qty, + @ForAll Metric metric, + @ForAll("euroMonetaryAmount") MonetaryAmount retailPrice, + @ForAll("euroMonetaryAmount") MonetaryAmount wholesalePrice, + @ForAll("optionalEuroMonetaryAmount") Optional promotionPrice, + @ForAll Set orderTypes) { + ConsumableMutateForm form = new ConsumableMutateForm(); + Assume.that(form.supportedMetrics().contains(metric)); + form.setName(name); + form.setQuantity(qty); + form.setMetric(metric); + form.setRetailPrice(retailPrice.getNumber().doubleValueExact()); + form.setWholesalePrice(retailPrice.getNumber().doubleValueExact()); + form.setPromotionPrice(promotionPrice.map(MonetaryAmount::getNumber).map(NumberValue::doubleValueExact)); + form.setOrderTypes(Set.of(OrderType.PARTY_SERVICE)); + + Product product = form.toProduct(); + UniqueInventoryItem item = new UniqueInventoryItem(product, product.createQuantity(qty)); + assertThat(ConsumableMutateForm.of(item)) + .usingRecursiveComparison() + .isEqualTo(form); + } + + @Property + void property_formToRentableToFormIsIdentity(@ForAll @NotBlank String name, + @ForAll("positiveOrZero") long qty, + @ForAll Metric metric, + @ForAll("euroMonetaryAmount") MonetaryAmount retailPrice, + @ForAll("euroMonetaryAmount") MonetaryAmount wholesalePrice, + @ForAll Set orderTypes) { + RentableMutateForm form = new RentableMutateForm(); + Assume.that(form.supportedMetrics().contains(metric)); + form.setName(name); + form.setQuantity(qty); + form.setMetric(metric); + form.setRetailPrice(retailPrice.getNumber().doubleValueExact()); + form.setWholesalePrice(retailPrice.getNumber().doubleValueExact()); + form.setOrderTypes(Set.of(OrderType.PARTY_SERVICE)); + + Product product = form.toProduct(); + UniqueInventoryItem item = new UniqueInventoryItem(product, product.createQuantity(qty)); + assertThat(RentableMutateForm.of(item)) + .usingRecursiveComparison() + .isEqualTo(form); + } + + @Property + void property_consumableToFormToConsumableIsIdentity(@ForAll("consumableItem") UniqueInventoryItem item) { + InventoryMutateForm form = InventoryMutateForm.of(item); + + assertThat(form).isInstanceOf(ConsumableMutateForm.class); + assertThat(form.toProduct()) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(item.getProduct()); + assertThat(form.getQuantity()).isEqualTo(item.getQuantity().getAmount().longValueExact()); + } + + @Property + void property_rentableToFormToRentableIsIdentity(@ForAll("rentableItem") UniqueInventoryItem item) { + InventoryMutateForm form = InventoryMutateForm.of(item); + + assertThat(form).isInstanceOf(RentableMutateForm.class); + assertThat(form.toProduct()) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(item.getProduct()); + assertThat(form.getQuantity()).isEqualTo(item.getQuantity().getAmount().longValueExact()); + } + + @Property + void property_consumableFormModifiesExistingConsumable(@ForAll("consumableItem") UniqueInventoryItem item, + @ForAll("consumable") Consumable product) { + InventoryMutateForm.of(item).modifyProduct(product); + + assertThat(product) + .usingRecursiveComparison() + .ignoringFields("id") + .ignoringFields("metric") // metric can’t be modified (salespoint limitation) + .isEqualTo(item.getProduct()); + } + + @Property + void property_rentableFormModifiesExistingConsumable(@ForAll("rentableItem") UniqueInventoryItem item, + @ForAll("rentable") Rentable product) { + InventoryMutateForm.of(item).modifyProduct(product); + + assertThat(product) + .usingRecursiveComparison() + .ignoringFields("id") + .ignoringFields("metric") // metric can’t be modified (salespoint limitation) + .isEqualTo(item.getProduct()); + } + + /* + * Edge cases with jqwik + */ + + @Property + void property_ofIllegal(@ForAll("consumableItem") UniqueInventoryItem consumableItem, + @ForAll("rentableItem") UniqueInventoryItem rentableItem) { + assertThatThrownBy(() -> InventoryMutateForm + .of(new UniqueInventoryItem(new Product("no subclass", Money.of(0.0, EURO)), Quantity.of(1)))) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> ConsumableMutateForm.of(rentableItem)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> RentableMutateForm.of(consumableItem)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Property + void property_modifyIllegalProduct(@ForAll("consumableItem") UniqueInventoryItem consumableItem, + @ForAll("rentableItem") UniqueInventoryItem rentableItem, + @ForAll("consumable") Consumable consumable, + @ForAll("rentable") Rentable rentable) { + assertThatThrownBy(() -> InventoryMutateForm.of(rentableItem).modifyProduct(consumable)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> InventoryMutateForm.of(consumableItem).modifyProduct(rentable)) + .isInstanceOf(IllegalArgumentException.class); + } + + /* + * JUnit Unit tests + */ + @Test + void toConsumable() { + ConsumableMutateForm form = new ConsumableMutateForm(); + form.setName("Weißwurst (vegan)"); + form.setQuantity(10L); + form.setMetric(Metric.UNIT); + form.setRetailPrice(3.46); + form.setWholesalePrice(1.50); + form.setPromotionPrice(Optional.of(3.33)); + form.setOrderTypes(Set.of(OrderType.PARTY_SERVICE)); + assertThat(form.getQuantity()).isEqualTo(10L); + assertThat(form.toProduct()) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(new Consumable("Weißwurst (vegan)", Money.of(3.46, EURO), Money.of(1.50, EURO), + Optional.of(Money.of(3.33, EURO)), Set.of(OrderType.PARTY_SERVICE), Metric.UNIT)); + } + + @Test + void toRentable() { + RentableMutateForm form = new RentableMutateForm(); + form.setName("Kebab-Drehspieß „Der Gerät“ Alkadur"); + form.setQuantity(3L); + form.setMetric(Metric.UNIT); + form.setRetailPrice(20.0); + form.setWholesalePrice(10000.0); + form.setOrderTypes(Set.of(OrderType.EVENT_CATERING)); + assertThat(form.getQuantity()).isEqualTo(3L); + assertThat(form.toProduct()) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(new Rentable("Kebab-Drehspieß „Der Gerät“ Alkadur", Money.of(20.0, EURO), + Money.of(10000.0, EURO), Set.of(OrderType.EVENT_CATERING), Metric.UNIT)); + } + + @Test + void forProductType() { + assertThat(InventoryMutateForm.forProductType(Consumable.class)).isInstanceOf(ConsumableMutateForm.class); + assertThat(InventoryMutateForm.forProductType(Rentable.class)).isInstanceOf(RentableMutateForm.class); + + assertThatThrownBy(() -> InventoryMutateForm.forProductType(Product.class)).isInstanceOf(IllegalArgumentException.class); + } +}