We would like to unit-test the following code (S contains our localizations):
enum Gender {
male,
female,
diverse,
}
extension GenderName on Gender {
String getName(BuildContext context) {
switch (this) {
case Gender.male:
return S.of(context).modelProfileGenderMale;
case Gender.female:
return S.of(context).modelProfileGenderFemale;
case Gender.diverse:
return S.of(context).modelProfileGenderDiverse;
}
}
}
This means that we somehow need to pass a BuildContext to the method that already has the localizations loaded. Is this possible in any way? We could not find any way to mock this.
I guess you could create an artificial MaterialApp around the test or something? Every other StackOverflow question along similar lines seems to result in that. But here we don't want to test widgets, we just want to test classes and enums.
Related
I am new to dart/flutter and I am adding internationalization on my app.
As flutter don't define a proper way to implement internationalization (according to the video I watched), devs are free to do it in any way they think its better.
The video showed an implementation using json files. So on "lang" folder I have my json files with all strings and their respective translations.
Now imagine this is a large file with lots of entries. And I have to make a big change on my app, removing some strings, adding others and adding a new language. This is very error prone as I may forget to add some key-value entry in any of the json files. It happened sometimes and the error is thrown at runtime.
In java there is a way to enforce the dev don't forget to add the same values in all variations of the same property at compile time. Maybe it's not the best way to achieve this, but one way is using an enum like in the image below.
Now if I want to add a new language, I just add a new parameter in the enum constructor and there is no way to forget adding a translation because the code would not even compile.
To fix the error I just have to add the value on the Strings (I forgot the "es" case here):
And then I can use the values doing something like this (the code to find the locale using the context could even be moved to inside the .value() method):
So is there a good way to achieve this in dart/flutter? To enforce all the languages have all the translated texts at compile time?
My method looks like this:
First, define the interface, and its implementations:
abstract class StringsInterface {
String get greet;
}
class EnglishStrings implements StringsInterface {
String get greet => "Hello";
}
class SpanishStrings implements StringsInterface {
String get greet => "Hola";
}
Next, define a class to store an instance of the current locale, as well as configure which class to use, for which language code:
class Strings {
static StringsInterface current = EnglishStrings();
static late Locale _locale;
static Locale get locale => _locale;
static set locale(Locale locale) {
_locale = locale;
current = stringsForLocale(locale);
}
static StringsInterface stringsForLocale(Locale locale) {
switch(locale.languageCode) {
// Here is where you define which class gets used for which language code
case 'es': return SpanishStrings();
default: return EnglishStrings();
}
}
}
Set the locale for the Strings class at runtime:
void main() async {
/// load from your own future
/// (e.g. pull from a persisted value stored on device in SharedPreferences/UserDefaults)
Strings.locale = Locale(await Future<String>.value('en'));
runApp(const MyApp());
}
Finally, use like this:
Text(Strings.current.greet)
As of dart 2.17 you can now use enhanced enums with members:
enum Strings {
welcome(en: 'Welcome', pt: 'Bem-vindo'),
goodbye(en: 'Good bye', pt: 'Adeus');
final String en;
final String pt;
const Strings({
required this.en,
required this.pt,
});
String value(String lang) {
switch (lang) {
case 'pt':
return pt;
case 'en':
default:
return en;
}
}
}
For example, I expect <T extends type> can works like this:
Class Parent {
String data;
Parent({ this.data });
}
Class Child extends Parent {
Child({ this.data }) : Parent(data: data);
void showData() { print(data); }
}
T wrapper<T extends Parent>(String value) {
var result = T(data: value);
return result;
}
void main() {
var trial = wrapper<Child>("Hello world");
trial.showMessage(); // print "Hello world"
}
But turns out it gives me error at var result = T(data: value);, saying that T is not a function. When I specified , I expect that T can be operated like Parent class, and if I supplied its descendant like Child, the operation done will be Child instead. But the constructor will work either way because T extends Parent. Is such thing possible?
Constructors are not inherited. You know that already, because you wrote one yourself in your child class that does nothing but call the base class with the same parameters.
One could as well write a different constructor. So "X extends Y" says a lot about X, but it does not say anything about how the constructor of X looks (or whether it even has a constructor accessible in that scope). So a constructor call is not in the properties available to you when you specify your generic to "extend Y", because Y can do exactly nothing to make sure all it's derivates follow a specific construction method.
Different languages deal with the problem of "but how do I construct a new instance of my generic type" in different ways, but the underlying concept is common to almost all concepts of generics where the generic code is compiled before knowing the specific types of all T's handled. A constructor is not inherited in most OOP languages, therefor it is not guaranteed to be there for any "X extends Y" even if Y has it.
It might be easy to overlook when you have all your code in one compilation unit. The compiler should be able to figure it out, right? But your code might not be in a single compilation unit:
Codebase one:
Class Parent {
String data;
Parent({ this.data });
}
T wrapper<T extends Parent>(String value) {
var result = T(data: value);
return result;
}
At this point, the compiler has no idea what "Child" might look like. It cannot possibly determine that the child class that will be used in the Future has a constructor like that.
Codebase 2:
Class Child extends Parent {
Child({ this.data }) : Parent(data: data);
void showData() { print(data); }
}
void main() {
var trial = wrapper<Child>("Hello world");
trial.showMessage(); // print "Hello world"
}
Now, at this point, a compiler could figure out that the program it's given would actually work. Some concepts of generics do that, where generics cannot be compiled into independent libraries, they always come as source code, because only the final compiler producing the executable can determine whether it would work with a specific class. Flutter does not do this. Flutter needs the generic itself be valid for the constraints given.
All newer language's versions of generics have followed the path of knowing the constraints beforehand and only allowing code operating inside those constraints. And I think it's good because while it has it's shortcomings, it leaves less room for errors or cryptic error messages.
I implemented the new (official) localization for Flutter (https://pascalw.me/blog/2020/10/02/flutter-1.22-internationalization.html) and everything is working fine, except that I don't know how to get the translation for a variable key.
The translation is in the ARB file, but how can I access it?
Normally I access translations using Translations.of(context).formsBack, but now I would like to get the translation of value["labels"]["label"].
Something like Translations.of(context).(value["labels"]["label"]) does not work of course.
I don't think this is possible with gen_l10n. The code that is generated by gen_l10n looks like this (somewhat abbreviated):
/// The translations for English (`en`).
class TranslationsEn extends Translations {
TranslationsEn([String locale = 'en']) : super(locale);
#override
String get confirmDialogBtnOk => 'Yes';
#override
String get confirmDialogBtnCancel => 'No';
}
As you can see it doesn't generate any code to perform a dynamic lookup.
For most cases code generation like this is a nice advantage since you get auto completion and type safety, but it does mean it's more difficult to accommodate these kinds of dynamic use cases.
The only thing you can do is manually write a lookup table, or choose another i18n solution that does support dynamic lookups.
A lookup table could look something like this. Just make sure you always pass in the current build context, so the l10n code can lookup the current locale.
class DynamicTranslations {
String get(BuildContext context, String messageId) {
switch(messageId) {
case 'confirmDialogBtnOk':
return Translations.of(context).confirmDialogBtnOk;
case 'confirmDialogBtnCancel':
return Translations.of(context).confirmDialogBtnCancel;
default:
throw Exception('Unknown message: $messageId');
}
}
}
To provide an example for https://stackoverflow.com/users/5638943/kristi-jorgji 's answer (which works fine):
app_en.arb ->
{
"languages": "{\"en\": \"English\", \"ro\": \"Romanian\"}"
}
localization_controller.dart ->
String getLocaleName(BuildContext ctx, String languageCode) {
return jsonDecode(AppLocalizations.of(ctx)!.languages)[languageCode];
}
getLocaleName(context, 'ro') -> "Romanian"
You can store a key in translation as json string.
Then you read it, parse it to Map<string,string> and access dynamically what you need.
Been using this approach with great success
I try to learn to flutter but notions are quite difficult for me.
I was working with local json files which I load at the start and then do calculations with the values inside.
But I see everywhere that everyone uses class to parse JSON.
Classes are a fairly complex concept for me. I find it more complicated to handle.
In my example if I get a particular user and I want to add a KEY / VALUE to it, I can't do it if I go through a class. How to do ?
Is it useful to use class if I just load json files and retrieve values inside to do statistics?
My homeController.dart
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
import 'package:myapp/widgets/users.dart';
Future<Map<String, dynamic>> loadJson() async {
final myJson = await rootBundle.loadString('assets/jsons/myjson.json');
return {
'myJson': myJson,
};
}
class HomeController extends StatefulWidget {
HomeController({Key key, this.title}) : super(key: key);
final String title;
#override
_HomeControllerState createState() => _HomeControllerState();
}
class _HomeControllerState extends State<HomeController> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
future: loadJson(),
builder: (context, snapshot) {
if(snapshot.hasError) print(snapshot.error);
if (!snapshot.hasData) {
return LinearProgressIndicator();
}
print('WITH CLASS =====================================');
var myJson1 = jsonDecode(snapshot.data['myJson'])['user'] as List;
List<User> myList1 = myJson1.map((e) => User.fromJson(e)).toList();
List selectedInList1 = myList1.where((e) => e.arg2>= 250).toList();
User chooseObject1 = selectedInList1.first;
chooseObject1.putIfAbsent('newKey', () => 'test'); // ???????????????????????
print(chooseObject1);
print('WITHOUT CLASS =====================================');
var myJson = json.decode(snapshot.data['myJson']) ;
List myList = myJson['user'];
List selectedInList = myList.where((e) => e['arg2'] >= 250).toList();
Map chooseObject = selectedInList.first;
chooseObject['newKey'] = "test";
print(chooseObject);
// Some code ...
My json
{
"user" : [
{
"arg1" : 1,
"arg2" : 200
},
{
"arg1" : 1,
"arg2" : 250
},
{
"arg1" : 1,
"arg2" : 300
}
]
}
My User Class
class User{
final int arg1;
final int arg2;
User(
this.arg1,
this.arg2
);
factory User.fromJson(dynamic json){
return User(
json['arg1'] as int,
json['arg2'] as int
);
}
#override
String toString() {
return '{ ${this.arg1}, ${this.arg2} }';
}
}
Studying a framework, library, engine, a new programming language, other technology (any abstraction) without knowing something that is required to know to be able to use it is more difficult and longer (a priori) task. It's like studying physics without some math knowledge. Of course, it doesn't mean that it's impossible. More than that many people are learning like that all the way. It's like not learning from bottom to top, but from top to bottom. It means that you want to make something, and to do it, you don't learn everything and then do it, but trying to do something, then learn the next things that a required to go closer to your goal. And everything is OK with that method. Everyone decides which method fits him better.
Everyone and everywhere use classes because classes in OOP languages (and Dart is an OOP language) are one of the base blocks using which we are creating our programs. More than that, I should mention, that all your program is written with classes. Classes are everywhere in Dart, practically everything is a class in Dart (if not everything literally).
You've said that you can't add a key/value to your class. You can't because you've not added such a behavior to your User class. Map is also just a class (that has been written for you by Dart creators). And it doesn't mean that you could not write something like that. You can just add a method (or operator overloading) to accomplish this task. It could be like that:
class User{
// ...
void add(String key, dynamic value) {
// ... implementation
}
}
And then you could do just the same.
Another open question is why do you really need to add a new key/value pair to your User? What problem that ability will help you to solve? Maybe you just declare this field in the User class? Or maybe you need to define another class? I couldn't answer this question, because I don't know what are you going to achieve.
So, I suppose the real question is: why should everyone prefer parse JSON data into custom classes instead of using a predefined Map class? There are many reasons to do that. First of all: it is to be able to reason about (and not only for humans but for a compilers either). What does Map class represents? It represents a collection of key and value pairs. No more no less. So there is no guarantee of what a particular collection contains? It could be empty, or it could contain something else or whatever. And working with such a collection is more difficult and error-prone (you are to remember everything by yourself, and the compiler will not help you). In our case, we could say that a map is raw data, i.e. a collection of unknown raw data. We do not really know what it is exactly until we'll check it. But when we define a class for our data we have not just a collection of raw data but a collection of User (a collection with known keys (data) and behavior (methods)). No more no less, with some guarantees. Like every object in that collection has a type of User. Every object has all fields that are declared in the User class. All of those fields have a specified type. It is very convenient. And now we not only know with what data we are working but the compiler either. And it will help us all the time. It will show type errors, it will generate more reliable and fast programs. And many other advantages. For example, when you access a property like that: e['arg2'] a compiler doesn't know whether the e contains such a key or not. But when you access to property like that: e.arg2 than the compiler 100% sure that such a key exists. Then, for example, let's imagine that we need to change a key name for some reason. In the first example, we can change it in one place and we can forget (easily) in every other place. So the program wouldn't work well in such a case. If we change a key name in the User class we wouldn't able to compile a program until we change every line of code with the old key name. It's also a huge difference. Of course, when you are writing a small program like that it's not a big deal to change it and to remember everything. But software complexity grows extremely fast and even in a medium program, it becomes a real problem. 20% of the time we are writing a program and 80% we are debugging it and fixing bugs. Classes allow us to greatly reduce the number of things that we should think about ourselves.
Of course, there are many other reasons (and it's difficult to put them all down in one answer), it's just a simple example to answer why. You can continue to write programs in a way that is convenient for you. It's OK. Some day, with some experience you will grasp the advantages of classes either. If you want to have some deeper know you are to read some OOP book and have some practice with it.
I like the way Catch has nested hierarchies of tests, and it works through the combinations. It feels more natural than the setup/teardown of xUnit frameworks.
I now have a set of tests. What I want to do, about halfway down is insert a load/save serialization test, and then repeat all the tests below that point, first without the load/save, then again using the data it loaded from the serialization process. I.e. to prove that the load/save was correct.
I cannot get my head around if Catch has anything that can help with this? If it was phpUnit, I would be thinking about a string of #depends tests, and use a #dataProvider with a boolean input. A bit ugly.
(If that does not make sense, let me know, and I'll try to work out a minimal example)
The issue here is that Catch is designed to descend a tree-like organisation of tests and it automatically discovers all of the leaf-nodes of the structure and calls back into the test cases with untested code paths until they're all tested. The leaf nodes (tests, sections) are meant to be independent.
It sounds like you want to test a repository - something that can persist some data and then load it back in.
To repeat the exact same tests in two different scenarios (before serialisation, after serialisation) you'd need to put the same tests into some common place and call into that place. You can still use the same Catch macros in a non-test-case function, as long as you call it from a test case.
One possible way to do this is:
struct TestFixture {
Data data;
Repository repository;
TestFixture() : data(), instance() { }
};
void fillUpData(Data& data) {
// ...
}
void isDataAsExpected(Data& data) {
// Verify that 'data' is what we expect it to be, whether we
// loaded it or filled it up manually
SECTION("Data has ...) {
REQUIRE(data...);
}
}
TEST_CASE_METHOD(TestFixture, "Test with raw data") {
fillUpData(data);
isDataAsExpected(data);
REQUIRE(repository.save(data));
}
TEST_CASE_METHOD(TestFixture, "Operate on serialised data") {
REQUIRE(repository.load(data));
isDataAsExpected(_data);
}
One possible alternative is to supply your own main and then use command-line arguments to control whether/not the data is first serialised.
There's a third way I can think of that uses a non-quite-ready-yet feature of Catch - Generators:
TEST_CASE("...") {
using Catch::Generators;
int iteration(GENERATE(values(0, 1)));
const bool doSave(iteration == 0);
const bool doLoad(iteration == 1);
Repository repository;
Data data;
if (doLoad) {
REQUIRE(repository.load(data));
} else {
// fill up data
}
REQUIRE(..data..test..);
if (doSave) {
REQUIRE(repository.save(data));
}
}
The advantage of this method is you can see the flow and the test runs twice (for the two values) but the major disadvantage is that Generators are incompatible with SECTIONs and BDD-style features.